一、python中的迭代器与生成器
可迭代对象 Iterable
将可以通过迭代工具(如for,list(),tuple()
等)迭代的对象称为可迭代对象Iterable,可以通过collections.abc
中的Iterable
类,使用isinstance(a, Iterable)
来判断a是否为可迭代对象,所有的iterable都是Iterable类的继承类的实例,所有的可迭代对象都支持__iter__
方法。
迭代器 Iterator
迭代器是一个实现了迭代器协议的对象,任何的可迭代对象都支持的__iter__()
方法就将可迭代对象转换为迭代器对象,使用isinstance(object,abc.Iterator)
来判断对象是否是迭代器对象,所有的迭代器都支持__next__()
方法,迭代器最核心的功能就是将原本的序列转换为惰性的。
迭代器的作用:①为遍历不同的聚合结构提供一个统一的接口;②访问一个聚合对象的内容而无需暴露它的内部表示。
迭代(for循环,list转换等)的作用流程
①检测被迭代对象是否是Iterable,若是则调用其__iter__()
方法将其转换为Iterator,若已经是Iterator则跳过此步,若以上两者都不是则抛出异常;
②对Iterator调用其__next__()
方法返回序列的第一个元素,对其进行操作(若是list转换则将元素加进列表),重复此操作直至处理完序列的所有元素;
③当元素处理完后,__next__
方法会抛出StopIteration异常,此时迭代工具会捕捉此异常并进行处理,然后停止迭代,返回结果。
注:①Iterable与Iterator是两种不同的类型,Iterable支持__iter__
方法不支持__next__
方法,Iterator支持__next__
方法不支持__iter__
方法;
②双下划线的方法为魔法方法,其可以使用obj.__next__()
方式调用也可以直接以next(obj)
的方式调用;
③迭代器对象是可迭代对象的转换,其允许将数据分批读取防止爆内存,但迭代器对象一经迭代就耗尽,若要继续使用必须重新生成;
④打开文件的字节流即是Iterable也是Iterator。
生成器 generator
python中的生成器是一种特殊的迭代器,首先其满足迭代器的所有功能和特性,其次生成器是在序列的生成上进行了改进。
迭代器一般是由可迭代对象转换而来(也可以自定义迭代器,但相对繁琐),因此其序列内容往往是事先固定的,而生成器简单来说,就是保存了序列中元素产生的方法,在需要的时候根据这个方法生成元素,因此节省了大量的存储空间,与迭代器类似的,生成器一经迭代就销毁。
生成器的创建方法
①元组推导式
列表、字典、集合推导式都是直接根据传入的函数和参数产生对象并将其转换好后返回,而元组推导式产生的是一个生成器,对于生成器(迭代器)来说,其不会显式的将序列的元素表示出来,只有调用其next方法时才会抛出序列的第一个元素,后续与迭代器相同,不再赘述(在很多情况下,并不区分迭代器与生成器);
②yield关键字
yield关键字是一个类似于return的关键字,其在函数中的作用为:当next方法被调用时,将当前值返回,然后阻塞等待,直到下一次next被调用时,函数回复它刚刚脱离的位置(即yield语句的位置,记忆最后一次执行的位置和所有的数据值)并继续执行直到下一个yield。
使用yield关键字的函数就是一个生成器,这个生成器序列中的元素就是yield返回的值,每调用一次next,函数就运行一段,直到yield语句,然后阻塞等待。
yield生成器的注意事项
①yield生成器中可以存在return语句,当执行至return时,生成器对象抛出StopIteration异常,会将return后的内容作为异常的详细信息显示;
②next方法操作生成器方法与上述迭代器相同,其返回值就是yield后的内容,在生成器中还存在send方法,send方法只有obj.send('aa')
这一种调用方式,其作用为给yield语句本身赋值,常用于在迭代过程中改变生成器对象,例如:
def f():
while True:
res = yield 1
if res == 3:
yield 2
此时就可以使用send(3)来使生成器的下一个元素改为2,注意send()
不能对一个还没有使用next语句的生成器使用,但是send(None)
可以,即若是一个新生成器,第一次迭代时若使用send方法则必须obj.send(None)
。
注:①对迭代器进行切片操作,使用标准库中的itertools.islice,它能返回一个迭代对象切片的生成器,islice(obj, start, end)
,其也是一个迭代工具,即会消耗掉迭代器对象;
②迭代器和生成器一经使用就销毁,必须重新生成,不能重复利用;
③与返回值类似的,生成器和迭代器对象的传递的是引用和方法,如果一个生成器对象yield一个可变对象(如yield一个列表,则在生成器对象中传递的都是引用,此时使用list,a.append()等不直接表明值的方法,其产生的列表会重复),则其值很可能会重叠为最后一个,这是python中可变对象的陷阱,一定要注意,在return/yield一个可变对象(尤其还是以变量引用的形式)时要格外小心;
④生成器保存的是生成元素的方法,这句话是正确理解生成器用法的最关键的注意事项,例如:
def primes1():
yield 2
it = list()
n = 2
for i in range(5):
yield i
it.append(lambda x: x+i)
yield it
那么保存在此生成器序列中的匿名函数是相同的,虽然占用了不同的内存,但是其生成方法就是lambda x: x+i
,在调用时,会自动去各级命名空间中寻找i,最终找到i=4,因此对于该生成器中的匿名函数而言,其形式就是lambda x: x+4
,此问题与③中情况有区别也有联系,应着重分析。
在python详解(4)–函数一文中,对于reduce函数的举例其结果会出现错误,其原理即上述分析过程,优化方式即将当前的n同步传入匿名函数,有兴趣可自行测试结果。
二、同步/异步/阻塞/非阻塞与IO模型
一些基础理论(复习)
阻塞:阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。
常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。
非阻塞:程序在等待某操作过程中,自身不被挂起,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。
非阻塞并不是在任何程序级别、任何情况下都可以存在的,仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。
非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。
同步:不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的,即有序状态,指代码调用 IO 操作时,必须等待 IO 操作完成才返回的调用方式。
异步:为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的,即无序,指代码调用 IO 操作时,不必等 IO 操作完成就返回的调用方式。
注:①阻塞与非阻塞是对线程的状态的描述,例如在线程运行过程中,有一个等待操作(文件IO等),阻塞即线程等在这里,非阻塞即线程可以离开去做别的事情;
②同步与异步是对函数/任务的描述,例如在线程运行过程中,某个函数有一个等待操作(文件IO等),同步即该函数必须等待操作完成后才能返回结果,异步即该函数不必等待操作完成即可返回结果。
一些IO模型
如上所述,在实际任务执行过程中必然会遇到IO操作必须等待的情况,针对这种情况人们提出了一些模型,用于处理此类问题。
(1)阻塞式IO(同步阻塞)
在发起 IO 请求后,当前线程会阻塞,直到响应请求。当前线程阻塞后,就会造成 CPU 等待,造成 CPU 资源浪费。
(2)非阻塞式 IO(同步非阻塞)
当发起 IO 请求后,需要不停的发起请求询问是否完成响应,通常这种询问是放在 while 循环里面完成的,但是,while 循环是非常耗费 CPU 的。而之前的阻塞式 IO 是不需要耗费 CPU的,它只是当前线程被阻塞了而已。
(在同步非阻塞的情况下,在发送while循环的过程中线程还是可以去做别的事情的,只是当前函数一直没有返回,仍需要线程一直询问)
(3)复用 IO(异步阻塞)
即select/epoll方法,监视多个socket/函数的状态,在监视过程中整个进程是被select/epoll方法阻塞的,但其好处在于单个进程就可以同时处理多个网络连接的IO,此种方法在linux操作系统中大量的使用,在很多web服务器上也有大量的应用,其着重点就是同时监视多个socket的状态,在链接数很少的情况下性能可能不升反降。
对于单个socket/函数而言,其是非阻塞的,因此对于进程而言,其是异步的,在该进程阻塞的过程中,其会主动让出CPU。
(4)信号驱动式 IO(异步阻塞)
借助“信号”机制可以实现有读写事件的时候主动通知,其实可以说是一种异步非阻塞的模型,但是在具体实现过程中有种种限制(如信号队列的数量),应用较少。
(5)异步 IO(异步非阻塞)
真正的异步IO,最理想化的模型,节省了通知准备好数据报请求的时间,在操作系统和一些高并发框架中由于种种的限制应用很少,且性能并没有明显的提升(与异步阻塞相比,因为在高并发的情况下,阻塞模型的进程也是在不断的处理请求),且编程难度提升很多。
即便整个I/O行为是非阻塞的还是需要有一个办法知道数据是否读取/写入成功,即必须有一个事件通知机制(而这个机制的进程是使用epoll的方式实现的),其实现本质与IO复用是相同的。
注:①上述模型是按照Unix的IO模型进行介绍的,其中的进程也可作为线程看待,重点是理解原理,这些模型是在原理之上建立起来的处理任务的方法,而非对于同步/异步,阻塞/非阻塞概念的描述,具体的实现十分复杂,牵涉到操作系统底层,后补;
②当前绝大部分异步多任务的实现方式都是IO复用。
图片引自https://blog.youkuaiyun.com/kisslotus/article/details/85109027。
select、poll、epoll方法
三者都是IO多路复用的机制,其中:
①select方法具有良好的跨平台支持,其监视三类文件描述符,读、写、异常,一般有最大链接限制,通过轮询来实现对多个文件描述符的监控;
②poll方法与select方法类似,但没有最大链接限制,封装了监视方法,但也是通过轮询来监视;
③epoll方法将轮询机制改为了事件通知机制,没有最大数量限制,也节省了时间,但只能在linux中使用;
epoll 并一定就比 select 好:
在高并发但连接活跃度不是很高的情况下, epoll比select好;
若并发不高,同时连接很活跃, select比epoll好。
几种响应的方式(服务器/操作系统)
①轮询,即数据拉取,在服务器端的含义为客户端在特定的时间间隔不断的向服务器发出请求,服务器做出响应,由于响应头很长,因此占用带宽并且不能及时更新;在操作系统端的含义为操作系统对列表中的文件描述符进行迭代,依次询问有无更新;两者在有大量需响应的用户时都会产生性能缺失;
②长轮询,建立连接后客户端发送请求,服务器端不马上回应,而是等待有数据更新或是连接超时时再回应,连接断开后客户端继续发出请求再建立连接,循环往复,相比较轮询减少资源浪费;
③SSE(Server Sent Event)是HTML5提出的一个标准,客户端与服务器之间创建TCP连接并且维持,客户端会定时发送请求至服务器询问,没有做到服务器端的实时推送;
④WS(Web Socket)全双工通信,连接建立后维持,客户端可以随时发送请求,服务器在有数据更新时也会实时推送,节省流量,两端都有监听socket来进行负责。
epoll的原理:
①操作系统在遍历文件描述符时是向应用程序的内存中复制一份到自己的内存中,当数量很大时,这个复制的动作会很耗费事件,因此在操作系统和应用程序之外新建一个内存用于存放文件描述符,节省了复制的时间,称为内存映射技术;
②将轮询机制改为事件通知机制,即类似于上述WS,当某一个文件描述符有更新时进行通知,其他的挂起,操作系统不会主动去询问。
注:(epoll有两种工作机制,LT(Level triggered)水平触发,ET(Edge triggered)边缘触发)LT即当一个文件操作符就绪而不做任何动作,内核会继续通知这个文件就绪,ET为高速工作方式,即一个文件操作符就绪只会通知一次,不会继续通知,因此相对易出错,Nginx默认采用ET。
三、python中的协程
协程
又称微线程,纤程,英文名Coroutine,协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。协程只有一个线程,因此不存在全局上下文切换和全局变量冲突问题。
协程的优点
①因为多协程位于同一线程中,其调度开销只有函数的寄存器上下文和栈,因此与线程相比,CPU的调度开销极低,这一点尤其在多线程/协程时体现的更明显,切换效率极高;
②协程比线程更轻量,一个Python线程大概占用8M内存,而一个协程只占用1KB不到内存;
③同样由于多协程位于同一线程中,无论如何都是并发的执行,因此不存在同步问题,无需对全局变量加锁,进一步的提升了效率。
综上,当需要同时执行的任务越多时,多协程越能体现出优势(高切换速度与轻量)。
coroutine与goroutine
C#、Lua、Python 语言都支持 coroutine 特性,Golang支持goroutine特性。
两者都是协程的意思,都可以称为微线程,其区别在于:
goroutine是抢占式多任务;
coroutine是协作式多任务;
熟悉就对了,在最早期的单核操作系统中,就有这么两种多任务的方式,将概念复用到线程与协程上,就是这两种协程的作用机理。
coroutine 的运行机制属于协作式任务处理,其对于线程的控制是由其自己决定的(假如它乐意一直占着那么它就可以一直占着),这种方式有优点(减少操作系统检测与调度的消耗)也有缺点(如上所述);
goroutine 属于抢占式任务处理,其已经和现有的多线程和多进程任务处理非常类似,应用程序对 CPU 的控制最终还需要由操作系统来管理。
此外,goroutine可以利用多核CPU,即其可以并行式执行;而coroutine必然是顺序的,因为coroutine中多协程必然位于同一线程下,必然是并发。
注:①协作式与抢占式只是一种多任务的机制,其与同步、异步无关,不论是协作式还是抢占式,都是可以实现异步的(就是当出现IO操作时上交CPU/线程的使用权或由操作系统调度收回CPU/线程的使用权),goroutine其默认状态就是异步,多个任务共同执行时并不会按顺序等待IO操作;
②上述所有同步/异步、阻塞/非阻塞、协作式/抢占式都是在单线程/CPU的情况下讨论的,coroutine也只支持单线程/单核的高并发异步,但goroutine与多线程是支持多核CPU的,即goroutine更类似于轻量级的多线程;
③coroutine无论如何都是有一个操作顺序的,这个顺序受人为或IO时间控制,但必然存在操作顺序,也因此可以不用考虑同步问题,goroutine由于其并行原因,顺序并不受控。
python中协程的基本实现方式
在生成器中讲述了yield关键字的用法,其可以保存当前函数状态,并中断函数执行,将此思想复用到多个生成器中,使多个函数并发执行,让线程在多个函数中轮转执行,这就是协程的最初思想。
举例如下:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
在该例中,将生成器c作为对象传入p,使用send方法令线程在两个函数中轮转执行。
协程的发展历史
(1)同步阻塞:即顺序执行,并且当IO事件发生时等待,会使CPU长久的处于空闲状态;
(2)多进程:可改善情况,当IO事件发生时切换进程,但进程切换开销大,且进程本身占据资源多,不适合在高并发的条件下使用;
(3)多线程:可改善情况,但在python中无法利用多核CPU资源,且所有的多线程都是抢占式多任务,无法确定下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么,可能出现竞态条件(race condition,是指设备或系统出现不恰当的执行时序,而得到不正确的结果);
(4)同步非阻塞单线程:虽然函数不再阻塞主程序,空出来的时间段CPU没有空闲着,但其并没有利用好这空闲去做其他有意义的事情,而是在循环尝试读写 socket (不停判断非阻塞调用的状态是否就绪),还得处理来自底层的可忽略的异常,也不能同时处理多个 socket ,就结果来说还不如同步阻塞;
(5)异步:①将对多个socket判断的操作封装,即select和epoll方法,使判断socket状态的工作由操作系统来做,当发现状态改变时,调用回调函数;
②回调函数callback,让epoll代替应用程序去监听多个socket状态,当状态变化时调用回调函数,回调函数必须是事先定义好并绑定于各个socket状态的,如"如果socket状态变为可以往里写数据(连接建立成功了),请调用HTTP请求发送函数。如果socket 变为可以读数据了(客户端已收到响应),请调用响应处理函数"
;
③事件循环Event Loop,以循环的方式访问任务队列,以获得需要执行的事件/回调函数,并进行执行,执行后继续循环。
注:事件循环与上述的select函数的轮询机制是两个概念,select函数的轮询是对所有监听的socket进行访问查看其状态,而事件循环是线程对任务队列进行访问,访问一次执行一次,在全部任务结束前要访问很多次,因此称为事件循环。
所有的异步多任务都是通过callback+epoll+Event loop的方式实现的,暂时还没有更新的方法,但上述这种循环+回调的方法由很多缺点:①回调层次过多,完全破坏了代码的可读性和开发结构;②共享状态管理十分困难;③错误处理困难,为了解决这些问题,python的开发者们做了非常多的努力。
python中的协程实现
上述已经举例说明了python中协程的基本实现方式(yield),但若要使线程可以在多个协程中转换,则必须嵌套生成器,Python 3.3 新引入的语法(PEP 380)提供了yield from表达式,其有以下作用:①让嵌套生成器不必通过循环迭代yield,而是直接yield from;②在子生成器和原生成器的调用者之间打开双向通道,两者可以直接通信,即起到一个桥梁的作用,两者之间通过yield 和send,throw等方法直接通信;③还有许多额外的功能,后续参考源码。
def gen():
yield from subgen()
def subgen():
while True:
x = yield
yield x+1
def main():
g = gen()
next(g) # 驱动生成器g开始执行到第一个 yield
retval = g.send(1) # 看似向生成器 gen() 发送数据
print(retval) # 返回2
g.throw(StopIteration) # 看似向gen()抛入异常
yield与yield from与生成器与协程
python中的生成器是通过yield关键字实现的,而协程最初是基于生成器实现的,在协程中可能也会用到普通的生成器,若不加以区分,则很容易混淆,在asyncio模块中提供了coroutine装饰器用以识别协程(但非强制)。
总结如下:
①生成器和协程都是通过python中的yield的关键字实现的,其区别在于,生成器只会调用next来不断地生成数据,而协程却会调用next和send来返回结果和接收参数;
②生成器是用来生成数据的,而协程从某种意义上来说是消耗数据的,生成器通过迭代和yield发生数据,协程与迭代无关,其通过yield转交线程的控制权;
③虽然有以上区别,却仍然难以区分协程与普通的生成器,因此引入了yield from,一方面简化了代码结构提供了上述功能(因为协程中必然会调用yield from用以转交控制权),另一方面也使yield from专用于识别协程,不再使用yield去实现协程;
④Python 3.5 中新增了async/await语法(PEP 492),其用于显式的识别协程,称为原生协程,与yield from风格底层实现相同,且相互兼容,但仍建议在同一个项目中使用相同写法。
python中的asyncio库
Python 3.6 中,asyncio正式成为了标准库,与生成器版的协程相比,asyncio库:
①没有了yield或yield from,而是async/await
(解决了协程与生成器的混淆问题);
②没有了自造的loop(),取而代之的是asyncio.get_event_loop()
;
③无需自己在socket上做异步操作,不用显式地注册和注销事件,aiohttp库封装了关于服务器处理并发请求的操作,是一个基于asyncio实现的http框架;
④封装了结果与任务处理过程,使开发代码更少量更优雅。
asyncio库的一些基础概念
event_loop
:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
coroutine
:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
task
:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
future
:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。
async
:关键字(在python3.5之后才可以使用),用于定义一个协程,与@asyncio.coroutine
等价。
await
:用来挂起阻塞方法的执行,协程切换的标志,与yield from
等价。
一些常用的方法:
async定义协程,该函数调用返回一个coroutine对象,即协程对象;
async def execute(x):
print('Number:', x)
c = execute(1)
asyncio.get_event_loop()方法创建事件循环;
loop = asyncio.get_event_loop()
create_task()创建task对象;
task = loop.create_task(c)
run_until_complete()方法将协程注册到事件循环 loop 中,然后启动,若传入的是coroutine对象,则会先将其封装为task对象再进行注册,可以直接传入task对象;
loop.run_until_complete(coroutine)
ensure_future()不借助loop就可以将协程封装为task对象;
task = asyncio.ensure_future(coroutine)
add_done_callback()绑定回调函数;
task.add_done_callback(callback)
task.result()方法在协程运行完毕后可以获得协程的结果,即协程函数的return值;
result = task.result()
future对象(task对象是future的子类对象)有几个状态:
Pending 初始态;
Running 调用态;
Done 完成态;
Cacelled 取消态;
asyncio.wait()方法:
并发运行 aws 指定的 可等待对象 并阻塞线程直到满足 return_when 指定的条件;返回两个 Task/Future 集合: (done, pending),可通过遍历done集合并表用result()方法来获取每个task的结果。
asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
done, pending = await asyncio.wait(aws)
asyncio.gather()方法:
并发 运行 aws 序列中的 可等待对象。
如果 aws 中的某个可等待对象为协程,它将自动作为一个任务加入日程。
如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。
如果 return_exceptions 为 False (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。
如果 return_exceptions 为 True,异常会和成功的结果一样处理,并聚合至结果列表。
asyncio.gather(*aws, loop=None, return_exceptions=False)
results = await asyncio.gather(*aws)
简单来说,wait更注重task的状态(返回不同状态task的集合),gather更注重task的结果(返回所有任务的返回值)。
在python3.4-3.6版本,异步协程的简单流程为:
①定义协程,在协程内部使用await方式标志阻塞操作(可以嵌套协程,即在一个协程中await另外一个协程,可以是 await asyncio.wait(tasks)
);
②创建loop对象;
③创建task对象,可以使用asyncio.ensure_future()
创建也可以使用loop.create_task()
创建;
④将task对象注册到loop对象中并开始运行,如果是单个task可以不转换为task对象,但为了避免混淆,统一生成tasks列表后,使用.wait或.gather方法进行注册loop.run_until_complete(asyncio.wait(tasks))
;
⑤调用结果,关闭loop,loop.close()
。
注:①在python3.7版本,提供了更高级的封装:
asyncio.create_task()
,与asyncio.ensure_future()
类似;
asyncio.run()
,理想情况下应只调用一次,且无需再进行loop注册,如下所示:
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def main():
tasks = []
for i in range(10):
tasks.append(asyncio.create_task(foo()))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
②aiohttp
是一个基于asyncio实现的http框架,其中分别提供了服务器端和客户端的方法,提供了异步请求的方式,requests库只支持同步阻塞的请求方式。
aiohttp是一个相当完善的异步网络库,以后需要详细了解。
③在python中由于GIL的存在(后详),多线程并不能充分利用多核CPU,因此多进程+协程就成为了最高效的方式,aiomultiprocess
库,在python 3.6及以上才可以应用,使用进程池的方式将协程与多进程结合,相当给力。
四、进程、线程、协程的区别及其他协程框架
进程与线程的区别在上文中已经详细分析(网上可以搜到的都没有我详细(_)),此处只分析协程(此处的协程特指coroutine不包括goroutine)与它们的差别,协程又被称为微线程,其与单线程的差别很小,主要是体量。
①进程、线程由操作系统调度,coroutine由开发者或说程序本身调度,在用户空间内完成问题,即抢占式多任务和协作式多任务的差别,切换消耗:进程>线程>协程;
②coroutine是在单线程内完成的,不需要锁机制,必然是并发;
③举例来说的话,进程相当于流水线,线程相当于流水线上工人,协程相当于压榨工人时间。
python的异步协程框架
python有很多实现了异步IO的网络框架(当然,其全部都是IO多路复用即epoll),除了上述标准库中的asyncio与aiohttp外,还有很多第三方库:
①Tornado,其对于协程的定义和使用方法在原理上与asyncio是相同的,是基于epoll(或kqueue)的httpserver和httpclient,兼容性很好,相对简单;
②twisted,较稳定,但对于python3支持不好;
③Greenlet是底层实现了原生协程的 C扩展库,是实现了一个比较易用(相比yeild)的协程切换的库,但是greenlet没有自己的调度过程,所以一般不会直接使用;
④Eventlet在Greenlet的基础上实现了自己的GreenThread,实际上就是greenlet类的扩展封装,而与Greenlet的不同是,Eventlet实现了自己调度器称为Hub,Hub类似于Tornado的IOLoop,是单实例的,在Hub中有一个event loop,根据不同的事件来切换到对应的GreenThread;
⑤Gevent基于libev和Greenlet,不同于Eventlet的用python实现的hub调度,Gevent通过Cython调用libev来实现一个高效的event loop调度循环;
注:①EVentlet与Gevent都有自己的monkey_patch,在打了补丁后,完全可以使用python线程的方式来无感知的使用协程,减少了开发成本,但这个补丁可能会造成奇怪的异常,原则上应尽早使用;
②总的来说各有优劣,asyncio由于最新,虽然是标准库但对于其他接口来说不一定支持,生态不成熟;tornado相对简单但支持python3;twisted很稳定但对于python3支持不好;Gevnet是当前使用起来最方便的协程了,但是由于依赖于libev所以不能在pypy上跑,如果需要在pypy上使用协程,Eventlet是最好的选择;
③python大名鼎鼎的网络框架django和flask都是同步阻塞的,即它们不支持异步IO,若想实现异步,则Ⅰ可以使用分布式异步编程,使用类似 celery 的方式,将需要异步处理的东西发送到 worker 去处理,Ⅱ使用异步方式编程,而且要保证wsgi server支持……uWSGI文档async说明 开头给出了一个警告:如果你的app不是时间驱动的话,使用这种模式是不对的。说白了,uwsgi的事件模式其实对应的是后端的事件框架,例如用gevent选项,后端是gevent才有效,如果后端是django,其实怎么配置没有多大区别,并没有对django的wsgi做了异步操作;
④Tornado内置了支持异步的http服务器,其本身是单线程的异步网络程序,它默认启动时,会根据CPU数量运行多个实例;充分利用CPU多核的优势。
参考文章:
https://blog.youkuaiyun.com/jiulixiang_88/article/details/80881174
https://blog.youkuaiyun.com/historyasamirror/article/details/5778378
https://blog.youkuaiyun.com/lzy98/article/details/83246270