一、协程(重点:gevent) 二、IO多路复用
本节的主题是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)状况下实现并发, 为此咱们须要先回顾下并发的本质:切换+保存状态
cpu正在运行一个任务,会在两种状况下切走去执行其余的任务(切换由操做系统强制控制),
一种状况是该任务发生了阻塞,另一种状况是该任务计算的时间过长或有一个优先级更高的程序替代了它python
协程本质上就是一个线程,之前线程任务的切换是由操做系统控制的,遇到I/O自动切换,
如今咱们用协程的目的就是较少操做系统切换的开销(开关线程,建立寄存器、堆栈等,在他们之间进行切换等),
在咱们本身的程序里面来控制任务的切换。linux
ps:在介绍进程理论时,说起进程的三种执行状态,而线程才是执行单位,因此也能够将上图理解为线程的三种状态nginx
若是多个任务都是纯计算的,这种切换反而会下降效率。 为此咱们能够基于yield来验证。 yield自己就是一种在单线程下能够保存任务运行状态的方法,咱们来简单复习一下: #1 yiled能够保存状态,yield的状态保存与操做系统的保存线程状态很像,可是yield是代码级别控制的,更轻量级 #2 send能够把一个函数的结果传给另一个函数,以此实现单线程内程序之间的切换
经过yield实现任务切换+保存状态git
单纯的切换反而会下降运行效率程序员
那好,我就本身搞成一个线程让你去执行,省去你切换线程的时间,我本身切换比你切换要快不少,避免了不少的开销, 对于单线程下,咱们不可避免程序中出现io操做,但若是咱们能在本身的程序中(即用户程序级别,而非操做系统级别) 控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另一个任务去计算, 这样就保证了该线程可以最大限度地处于就绪态,即随时均可以被cpu执行的状态, 至关于咱们在用户程序级别将本身的io操做最大限度地隐藏起来,从而能够迷惑操做系统, 让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给咱们的线程。
协程的本质就是在单线程下,由用户本身控制一个任务遇到io阻塞了就切换另一个任务去执行,
以此来提高效率。为了实现它,咱们须要找寻一种能够同时知足如下条件的解决方案:github
#1. 能够控制多个任务之间的切换,切换以前将任务的状态保存下来,以便从新运行时,能够基于暂停的位置继续执行。 #2. 做为1的补充:能够检测io操做,在遇到io操做的状况下才发生切换
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。 一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序本身控制调度的。 对比操做系统控制线程的切换,用户在单线程内控制协程的切换 协程在操做系统上是没有这个概念的,是程序员们本身叫的
1. python的线程属于内核级别的,即由操做系统控制调度 (如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其余线程运行) 2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操做系统)控制切换, 以此来提高效率(!!!非io操做的切换与效率无关)
1. 协程的切换开销更小,属于程序级别的切换,操做系统彻底感知不到,于是更加轻量级 2. 单线程内就能够实现并发的效果,最大限度地利用cpu
1. 协程的本质是单线程下,没法利用多核, 能够是一个程序开启多个进程,每一个进程内开启多个线程,每一个线程内开启协程 2. 协程指的是单个线程,于是一旦协程出现阻塞,将会阻塞整个线程
一、必须在只有一个单线程里实现并发 二、修改共享数据不需加锁 三、用户程序里本身保存多个控制流的上下文栈 四、附加:一个协程遇到IO操做自动切换到其它协程 (如何实现检测IO,yield、greenlet都没法实现,就用到了gevent模块(select机制))
若是咱们在单个线程内有20个任务,要想实如今多个任务之间切换, 使用yield生成器的方式过于麻烦(须要先获得初始化一次的生成器,而后再调用send。。。很是麻烦), 而使用greenlet模块能够很是简单地实现这20个任务直接的切换 #安装 pip3 install greenlet
单纯的切换(在没有io的状况下或者没有重复开辟内存空间的操做),反而会下降程序的执行速度
效率对比web
greenlet只是提供了一种比generator更加便捷的切换方式, 当切到一个任务执行时若是遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提高效率的问题。
#安装 pip3 install gevent
Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程,
在gevent中用到的主要模式是Greenlet,
它是以C扩展模块形式接入Python的轻量级协程。
Greenlet所有运行在主程序操做系统进程的内部,但它们被协做式地调度。编程
协程:同步异步对比数组
协程应用:爬虫服务器
将上面的程序最后加上一段串行的代码看看效率: 若是你的程序不须要过高的效率,那就不用什么并发啊协程啊之类的东西。
经过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all() 必定要放到导入socket模块以前,不然gevent没法识别socket的阻塞)
一个网络请求里面通过多个时间延迟time
服务端
客户端
多线程并发多个客户端,去请求上面的服务端是没问题的
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么, 到底有什么区别?这个问题其实不一样的人给出的答案均可能不一样, 好比wiki,就认为asynchronous IO和non-blocking IO是一个东西。 这实际上是由于不一样的人的知识背景不一样,而且在讨论这个问题的时候上下文(context)也不相同。 因此,为了更好的回答这个问题,我先限定一下本文的上下文。
本文讨论的背景是Linux环境下的network IO。
本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,
6.2节“I/O Models ”,Stevens在这节中详细说明了各类IO的特色和区别,
若是英文够好的话,推荐直接阅读。
Stevens的文风是有名的深刻浅出,因此不用担忧看不懂。本文中的流程图也是截取自参考文献。
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO(不常见,不讲)
* asynchronous IO 异步IO
由signal driven IO(信号驱动IO)在实际中并不经常使用,因此主要介绍其他四种IO Model。
再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里咱们以read、recv举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read/recv读数据的操做发生时,该操做会经历两个阶段:
1)等待数据准备 (Waiting for the data to be ready) 2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO模型的区别就是在两个阶段上各有不一样的状况。
就是咱们日常写的input,的代码,这样的阻塞 在linux中,默认状况下全部的socket都是blocking, 一个典型的读操做流程大概是这样:(recvfrom和tcp里面的recv在这些IO模型里面是同样的)
上面的图形分析:两个阶段的阻塞
因此,blocking IO的特色就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
这里咱们回顾一下同步/异步/阻塞/非阻塞:
同步:提交一个任务以后要等待这个任务执行完毕
异步:只管提交任务,不等待这个任务执行完毕就能够去作其余的事情
阻塞:recv、recvfrom、accept,线程阶段 运行状态–>阻塞状态–>就绪
非阻塞:没有阻塞状态
在一个线程的IO模型中,咱们recv的地方阻塞,咱们就开启多线程,
可是无论你开启多少个线程,这个recv的时间是否是没有被规避掉,
无论是多线程仍是多进程都没有规避掉这个IO时间。
Linux下,能够经过设置socket使其变为non-blocking。 当对一个non-blocking socket执行读操做时,流程是这个样子: 在非阻塞式IO中,用户进程实际上是须要不断的主动询问kernel数据准备好了没有。 虽然咱们上面的代码经过设置非阻塞,规避了IO操做,可是非阻塞IO模型毫不被推荐。
非阻塞IO模型服务端
非阻塞IO模型客户端
非阻塞IO示例详细版
虽然咱们上面的代码经过设置非阻塞,规避了IO操做,可是非阻塞IO模型毫不被推荐。
咱们不可否则其优势:可以在等待任务完成的时间里干其余活了
(包括提交其余任务,也就是 “后台” 能够有多个任务在“”同时“”执行)。
1. 循环调用recv()将大幅度推高CPU占用率; 这也是咱们在代码中留一句time.sleep(2)的缘由,不然在低配主机下极容易出现卡机状况 2. 任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做, 而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。 此外,在这个方案中recv()更多的是起到检测“操做是否完成”的做用, 实际操做系统提供了更为高效的检测“操做是否完成“做用的接口, 例如select()多路复用模式,能够一次检测多个链接是否活跃。
先看解释图,里面的select就像个代理。 IO multiplexing这个词可能有点陌生,可是若是我说select/epoll,大概就都能明白了。 有些地方也称这种IO方式为事件驱动IO(event driven IO)。 咱们都知道,select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。 它的基本原理就是select/epoll这个function会不断的轮询所负责的全部socket, 当某个socket有数据到达了,就通知用户进程。它的流程如图:
1. 若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好, 可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。 2. 在多路复用模型中,对于每个socket,通常都设置成为non-blocking,可是, 如上图所示,整个用户的process实际上是一直被block的。 只不过process是被select这个函数block,而不是被socket IO给block。
select的优点在于能够处理多个链接,不适用于单个链接
io多路复用服务端
io多路复用客户端
select网络IO模型的示例代码详细解释版