http://www.woola.net/detail/2016-10-18-python-coprocessor.html
http://senlinzhan.github.io/2016/07/30/Python%E5%8D%8F%E7%A8%8B%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F/
https://blog.csdn.net/soonfly/article/details/78361819
Python由于众所周知的GIL的原因,
导致其线程无法发挥多核的并行计算能力
(当然,后来有了multiprocessing,可以实现多进程并行),
显得比较鸡肋。既然在GIL之下,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的:
多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复(I/O结束)
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器变形yield/send (生成器模拟协程)
- 引入@asyncio.coroutine和yield from
- 在最近的Python3.5版本中引入async/await关键字
当一个函数中包含yield语句时,python会自动将其识别为一个生成器。
yield 向外部传递数据(状态)
send从外部向生成器内部传递数据(状态)
这样就用yield和send模拟出了一个协程。
(协程https://www.cnblogs.com/my_life/articles/9075758.html 4.2 核心问题
协程就是能保存状态,能暂停,能恢复的函数
)
生成器不会把结果保存在一个系列中,而是保存生成器的状态,在每次进行迭代时返回一个值,直到遇到StopIteration异常结束。
一、生成器变形yield/send
普通函数中如果出现了yield关键字,那么该函数就不再是普通函数,而是一个生成器。
def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
yield alist.pop(c)
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
输出:<generator object mygen at 0x02E5BF00>
像上面代码中的c就是一个生成器。生成器就是一种迭代器,可以使用for进行迭代。生成器函数最大的特点是可以接受外部传入的一个变量,并根据变量内容计算结果后返回。
这一切都是靠生成器内部的send()函数实现的。
def gen():
value=0
while True:
receive=yield value
if receive=='e':
break
value = 'got: %s' % receive
g=gen() #创建生成器对象
print(g.send(None)) #启动生成器
print(g.send('hello'))
print(g.send(123456))
print(g.send('e'))
上面生成器函数中最关键也是最易理解错的,就是receive=yield value这句,如果对循环体的执行步骤理解错误,就会失之毫厘,差之千里。
其实receive=yield value包含了3个步骤:
1、向函数外抛出(返回)value
2、暂停(pause),等待next()或send()恢复
3、赋值receive=MockGetValue() 。 这个MockGetValue()是假想函数,用来接收send()发送进来的值
执行流程:
1、通过g.send(None)或者next(g)启动生成器函数,并执行到第一个yield语句结束的位置。
这里是关键,很多人就是在这里搞糊涂的。
运行receive=yield value
语句时,我们按照开始说的拆开来看,实际程序只执行了1,2两步,程序返回了value值,并暂停(pause),并没有执行第3步给receive赋值。
因此yield value会向外传递初始值0。 不过外部并没有捕获这个传出来的初始值。 这里要特别注意:在启动生成器函数时只能send(None),如果试图输入其它的值都会得到错误提示信息。
2、通过g.send('hello')
,会传入hello,从上次暂停的位置继续执行,那么就是运行第3步,把传入的“hello”赋值给receive。然后计算出value的值,并回到while头部,遇到yield value,程序再次执行了1,2两步,程序返回了value值,并暂停(pause)。
此时yield value会输出”got: hello”,并等待send()激活。
3、通过g.send(123456),会重复第2步,最后输出结果为”got: 123456″。
4、当我们g.send(‘e’)时,程序会执行break然后推出循环,最后整个函数执行完毕,所以会得到StopIteration异常。
从上面可以看出, 在第一次send(None)启动生成器(执行1–>2,通常第一次返回的值没有什么用)之后,对于外部的每一次send(),生成器的实际在循环中的运行顺序是3–>1–>2,也就是先获取值,然后dosomething,然后返回一个值,再暂停等待。
二、yield from
看一段代码:
def g1():
yield range(5)
def g2():
yield from range(5)
it1 = g1()
it2 = g2()
for x in it1:
print(x) #输出range(5)
for x in it2:
print(x)
输出:
range(0, 5)
0
1
2
3
4
这说明yield就是将range这个可迭代对象直接返回了。
而yield from解析了range对象,将其中每一个item返回了。 yield from iterable
本质上等于for item in iterable: yield item
的缩写版
来看一下例子,假设我们已经编写好一个斐波那契数列函数
def fab(max):
n,a,b = 0,0,1
while n < max:
yield b
# print b
a, b = b, a + b
n = n + 1
f=fab(5)
fab不是一个普通函数,而是一个生成器。因此fab(5)并没有执行函数,而是返回一个生成器对象(生成器一定是迭代器iterator,迭代器一定是可迭代对象iterable)
现在我们来看一下,假设要在fab()的基础上实现一个函数,调用起始都要记录日志
def f_wrapper(fun_iterable):
print('start')
for item in fun_iterable:
yield item
print('end')
wrap = f_wrapper(fab(5))
for i in wrap:
print(i,end=' ')
现在使用yield from代替for循环
import logging
def f_wrapper2(fun_iterable):
print('start')
yield from fun_iterable #注意此处必须是一个可生成对象
print('end')
wrap = f_wrapper2(fab(5))
for i in wrap:
print(i,end=' ')
再强调一遍:yield from后面必须跟iterable对象(可以是生成器,迭代器)
三、asyncio.coroutine和yield from
yield from在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。
先看示例代码:
import asyncio,random
@asyncio.coroutine
def smart_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.2)
yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作; yield from是用来调用另外一个协程的
print('Smart one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
@asyncio.coroutine
def stupid_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.4)
yield from time.sleep(sleep_secs) #通常yield from后都是接的耗时操作
print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
smart_fib(10),
stupid_fib(10),
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
yield from语法可以让我们方便地调用另一个generator。
本例中yield from后面接的asyncio.sleep()是一个coroutine(里面也用了yield from),所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
asyncio是一个基于事件循环的实现异步I/O的模块。
通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。
协程之间的调度都是由事件循环决定。
yield from asyncio.sleep(sleep_secs)
这里不能用time.sleep(1)
因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)。
所以会报错:
yield from time.sleep(sleep_secs)
TypeError: ‘NoneType’ object is not iterable
四、async和await
弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。
当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
加入新的关键字 async ,可以将任何一个普通函数变成协程
import time,asyncio,random
async def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
print(alist.pop(c))
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
输出:
<coroutine object mygen at 0x02C6BED0>
在上面程序中,我们在前面加上async,该函数就变成一个协程了。
但是async对生成器是无效的。async无法独自将一个生成器转换成协程,需配合await来将生成器变成协程。
还是刚才那段代码,我们把print改成yield
async def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
yield alist.pop(c)
a = ["ss","dd","gg"]
c=mygen(a)
print(c)
可以看到输出
<async_generator object mygen at 0x02AA7170>
并不是coroutine 协程对象
所以我们的协程代码应该是这样的
import time,asyncio,random
async def mygen(alist):
while len(alist) > 0:
c = random.randint(0, len(alist)-1)
print(alist.pop(c))
await asyncio.sleep(1) #紧接着await后面的是生成器
strlist = ["ss","dd","gg"]
intlist=[1,2,5,6]
c1=mygen(strlist)
c2=mygen(intlist)
print(c1)
要运行协程,要用事件循环
在上面的代码下面加上:
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
c1,
c2
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
就可以看到交替执行的效果。
本文参考
http://python.jobbole.com/81911/
http://python.jobbole.com/86069/
================================
从yield说起
yield在这里可以保留fib函数的计算现场,暂停fib的计算并将b返回。而将fib放入for…in循环中时,每次循环都会调用next(fib(20)),唤醒生成器,执行到下一个yield语句处,直到抛出StopIteration异常。此异常会被for循环捕获,导致跳出循环。
Send来了
从上面的程序中可以看到,目前只有数据从fib(20)中通过yield流向外面的for循环;如果可以向fib(20)发送数据,那不是就可以在Python中实现协程了嘛。
于是,Python中的生成器有了send函数,yield表达式也拥有了返回值。
yield from是个什么鬼?
yield from的作用还体现可以像一个管道一样将send信息传递给内层协程,并且处理好了各种异常情况
asyncio.coroutine和yield from
asyncio是一个基于事件循环的实现异步I/O的模块。通过yield from asyncio.sleep,我们可以将协程的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。
这样说可能比较抽象,好在asyncio是一个由python实现的模块,那么我们来看看asyncio.sleep中都做了些什么:
首先,sleep创建了一个Future对象,作为更内层的协程对象,通过yield from交给了事件循环;其次,它通过调用事件循环的call_later函数,注册了一个回调函数。
通过查看Future类的源码,可以看到,Future是一个实现了__iter__对象的生成器:
那么当我们的协程yield from asyncio.sleep时,事件循环其实是与Future对象建立了练习。每次事件循环调用send(None)时,其实都会传递到Future对象的__iter__函数调用;而当Future尚未执行完毕的时候,就会yield self,也就意味着暂时挂起,等待下一次send(None)的唤醒。
当我们包装一个Future对象产生一个Task对象时,在Task对象初始化中,就会调用Future的send(None),并且为Future设置好回调函数。
设的时间过后,事件循环将调用Future._set_result_unless_cancelled:
这将改变Future的状态,同时回调之前设定好的Tasks._wakeup;在_wakeup中,将会再次调用Tasks._step,这时,Future的状态已经标记为完成,因此,将不再yield self,而return语句将会触发一个StopIteration异常,此异常将会被Task._step捕获用于设置Task的结果。同时,整个yield from链条也将被唤醒,协程将继续往下执行。
async和await
弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
总结
至此,Python中的协程就介绍完毕了。示例程序中都是以sleep为异步I/O的代表,
在实际项目中,可以使用协程异步的读写网络、读写文件、渲染界面等,而在等待协程完成的同时,CPU还可以进行其他的计算。协程的作用正在于此。