Python层面中的高并发

一、传输模型

(一).基本模型

(二).层次划分

七层模型与四层模型

作为Python开发,都是在应用层的HTTP协议之上进行开发的。HTTP协议是基于TCP之上的,也就是Python开发需要关心的是传输层。

 

二、TCP连接

(一).建立连接(三次握手)

第一次,只是客户端告诉服务端。

第二次,客户端才知道服务端收到了。

第三次,服务端才知道客户端收到了。

(二).传输数据

客户端向服务端请求,服务端向客户端响应。

一发一收,一收一发。

(三).断开连接(四次挥手)

因为服务端可能还有数据要发给客户端,所以在断开时就多了一次挥手。

 

三、IP地址与端口

(一).IP

127.0.0.1(localhost):本地回环

0.0.0.0:任意IP都可以访问

(二).端口

端口是用来区分我们的应用程序的

端口的范围:0-65535,其中0-1023多为知名端口,1024-65535多为动态端口

 

四、套接字编程

(一).套接字基本概念

(二).三种套接字

(三).套接字编程的常用方法

绑定套接字:bind()

服务端监听套接字:listen()

客户端连接服务端套接字:connect()

对等连接套接字:accept()

发送数据套接字:send()

接收数据套接字:recv()

关闭连接套接字:close()

(四).示例

(1).服务端代码

# 服务端

import socket
from datetime import datetime

server = socket.socket()  # 实例化
server.bind(("127.0.0.1", 3333))  # 绑定
server.listen(16)  # 监听
print(server)

while 1:
    conn, addr = server.accept()  # 生成对等连接套接字
    print(f"time:{datetime.now()}")
    msg = conn.recv(1024)  # 接收来自客户端的数据
    print(f"got a message from client:{msg}")
    conn.send(msg)  # 把数据发送回去给客户端
    conn.close()  # 把对等连接套接字关了。服务不要关,不然监听不到了
View Code

(2).客户端代码

# 客户端

import socket

while 1:
    client = socket.socket()
    client.connect(("127.0.0.1", 3333))#连接
    sentence = input("enter your message:")
    msg = bytes(sentence, "utf-8")
    client.send(msg)
    response = client.recv(1024)
    print(f"have received response from server:{response}")
    client.close()
View Code

(3).先启动服务端

不启动服务器,客户端怎么连过去?比如某服务器崩了,还能访问吗?

 

五、非阻塞套接字

第四部分中的示例是阻塞套接字,一次只能服务一个客户端,在accept()和recv()这两处会发生阻塞。

(一).accept()阻塞

在没有新的套接字来之前,不能处理已经建立连接的套接字的请求。

(二).recv()阻塞

在没有接受到客户端请求数据之前,不能与其他客户端建立连接。

(三).普通服务器的IO模型

(四).非阻塞IO模型

 

六、IO多路复用

第五部分的非阻塞套接字,有着非常严重的资源浪费,CPU吃满,无用处理很多。

那么就需要考虑用更合适的技术来解决这个问题,这里使用Linux上的epoll,来解决这个问题。

epoll是目前Linux上,效率最高的IO多路复用技术!

(一).IO多路复用技术

把socket交给操作系统去监控。

(二).epoll是惰性的事件回调

惰性事件回调是由用户进程自己调用的操作系统只起到通知的作用

 

七、进程

(一).计算机执行指令示意图

(二).轮询调度实现并发执行

并发:看上去一起执行,同时在发生

并行:真正一起执行,同时在进行

调度算法:时间片轮转;优先级调度

前提:一个CPU

(三).并行需要的核心条件

并行真正的核心条件是有多个CPU

(四).多进程实现并行

(1).进程的概念

计算机程序是存储在磁盘上的可执行二进制(或其他类型)文件。

只有把它们加载到内存中,并被操作系统调用,它们才会拥有其自己的生命周期。

进程则是表示的一个正在执行的程序。

每个进程都拥有自己的地址空间、内存、数据栈,以及其他用于跟踪执行的辅助数据。

操作系统负责其上所有进程的执行。操作系统会为这些进程合理地分配执行时间。

(2).Python进程的使用流程

(3).多进程并行的必要条件

总进程数量不多于CPU核心数量!

运行的程序都是轮询调度产生的并行假象。但是在Python层面的确获得了并行!

 

八、线程

(一).线程的概念

线程被称作轻量级进程。与进程类似,不过它们是在同一个进程下执行的。并且它们会共享相同的上下文。

当其他线程运行时,它可以被抢占(中断)和临时挂起(也成为睡眠),也被成为:让步。

线程的轮询调度机制类似于进程的轮询调度。只不过这个调度不是由操作系统来负责,而是由Python解释器来负责。

(二).Python线程的使用流程

(三).GIL锁

全局解释器锁

Python在设计的时候,还没有多核处理器的概念。因此,为了设计方便与线程安全,直接设计了一个锁。这个锁要求,任何进程中,一次只能有一个线程在执行。

因此,并不能为多个线程分配多个CPU。所以Python中的线程只能实现并发,而不能实现真正的并行。

但是Python3中的GIL锁有一个很棒的设计,在遇到阻塞(不是耗时)的时候,会自动切换线程。因此我们可以利用这种机制来有效的避开阻塞,充分利用CPU

Django、Flask、Web2py,都是使用多线程来做。

 

九、并发通信

(一).进程间的通信

进程是互不干扰的独立内存空间

进程间通信的解决方案:

1.管理器负责与公共进程通信2.代理负责操作共享的空间

(二).线程间的通信

线程属于同一个进程,是共享内存空间的。会发生资源竞争的问题,需要用互斥锁来解决这个问题。

互斥锁:控制共享资源的访问

(三).线程与进程安全的队列

队列的基本概念:

一个入口,一个出口,先入先出,First input first output(FIFO)

 

十、进程池与线程池

(一).池的概念

主线程: 相当于生产者,只管向线程池提交任务。并不关心线程池是如何执行任务的。因此,并不关心是哪一个线程执行的这个任务。

线程池: 相当于消费者,负责接收任务,并将任务分配到一个空闲的线程中去执行。

 

十一、greenlet

(一).什么是greenlet

虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。

与此同时,Python的一个衍生版 Stackless Python 实现了原生的协程,它更利于使用。

于是,大家开始将 Stackless 中关于协程的代码单独拿出来做成了CPython的扩展包。

这就是greenlet的由来,因此 greenlet 是底层实现了原生协程的C扩展库

(二).安装包

需要额外安装依赖包:sudo pip3 install greenlet

(三).greenlet的价值

(1).高性能的原生协程

(2).语义更加明确的显式切换

(3).直接将函数包装成协程,保持原有代码风格

 

十二、gevent协程

(一).什么事gevent协程

虽然,我们有了 基于 epoll 的回调式编程模式,但是却难以使用。

即使我们可以通过配合 生成器协程 进行复杂的封装,以简化编程难度。

但是仍然有一个大的问题: 封装难度大,现有代码几乎完全要重写。

gevent,通过封装了 libev(基于epoll) 和 greenlet 两个库。

帮我们做好封装,允许我们以类似于线程的方式使用协程。

以至于我们几乎不用重写原来的代码就能充分利用 epoll 和 协程 威力。

(二).安装包

sudo pip3 install gevent

(三).gevent的价值

遇到阻塞就切换到另一个协程继续执行 !

(1).使用基于epoll的libev来避开阻塞

(2).使用基于gevent的高效协程来切换执行

(3).只在遇到阻塞的时候切换,没有轮询的开销,也没有线程的开销

posted @ 2018-11-26 20:15  root01_barry  阅读(726)  评论(0编辑  收藏  举报