# 协程进化史
# IO上下文
如果协程中涉及 IO 操作,则需要在 IO 未就绪时通过 yield 让出执行权。在让出执行权的同时,还需要将 IO 上下文提交给事件循环,由它协助处理。那么,IO 上下文需要包含哪些信息呢?
IOContext 需要保存哪些信息取决于封装程度,但至少要包括协程需要等待的 文件描述符 以及感兴趣的 事件 :
class IOContext:
def __init__(self, fileno, events):
self.fileno = fileno
self.events = events
现在我们开始编写一个带 IO 操作的协程,它负责从监听套接字接收新客户端连接:
def accept_client(sock):
while True:
try:
return sock.accept()
except BlockingIOError:
pass
yield IOContext(sock.fileno(), select.EPOLLIN)
协程主体逻辑是一个循环,它先调用 accept 尝试接收新连接。如果没有连接就绪,accept 会抛 BlockingIOError 异常。 这时,yield 语句让出执行权,并将 IOContext 提交给事件循环。注意到,协程对套接字上的读事件感兴趣。
现在我们创建一个这样的协程,并扮演事件循环,来体会协程调度过程。如果套接字 s 没有就绪连接,send 将收到协程返回的 IOContext ,表明协程期待哪些事件发生:
>>> co = accept_client(s)
>>> context = co.send(None)
>>> context
<__main__.IOContext object at 0x7fcd58e3ef70>
>>> context.fileno
3
>>> context.events
1
事件循环接到上下文后,需要将当前协程保存到上下文中,并将需要订阅的事件注册到 epoll :
>>> context.co = co
>>> ep.register(context.fileno, context.events)
接着,事件循环在 epoll 上等待相关事件到达:
>>> ep.poll()
poll 将保持阻塞,直到有注册事件出现。因此,用 telnet 命令再次连接 s 套接字,poll 将返回:
>>> ep.poll()
[(3, 1)]
根据 poll 返回的文件描述符 3 ,我们知道 context 这次 IO 操作已经就绪了。这时,可以接着调度对应的协程:
>>> context.co.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: (<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 55555), raddr=('127.0.0.1', 51528)>, ('127.0.0.1', 51528))
至此,协程 co 成功接收了一个客户端连接,并退出了。
# yield
开始研究有 IO 操作的协程之前,我们先拿一个纯计算协程练练手。这是一个只做加法运算的协程:
def add(a, b):
if False:
yield
return a + b
if 语句永远不会执行,它只是为了引入 yield 语句,让 Python 将 add 编译成生成器。
现在我们创建一个新协程,并调用 send 方法把它调度起来:
>>> co = add(1, 2)
>>> co.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 3
正如前面提到的那样,协程一把梭哈到底,StopIteration 异常告诉我们它已经执行完毕,结果是 3 。注意到,协程执行结果 (函数返回值) 保存在 StopIteration 的 value 属性:
>>> import sys
>>> e = sys.last_value
>>> e
StopIteration(3)
>>> e.value
3
我们还可以写一个函数来调度协程,函数只需调用 send 方法,并在协程执行完毕后输出一些提示:
def schedule_coroutine(co):
try:
co.send(None)
except StopIteration as e:
print('coroutine {} completed with result {}'.format(co.__name__, e.value))
>>> co = add(2, 3)
>>> schedule_coroutine(co)
coroutine add completed with result 5
# yield from
注意,这里还没有涉及到事件队列。
现有一个用于计算圆面积的协程,它没有涉及 IO 操作:
import math
def circle_area(r):
if False:
yield
return math.pi * r ** 2
创建一个这样的协程来计算半径为 2 的圆的面积,并调用 send 方法来调度它,协程执行完毕后将返回结果:
>>> co = circle_area(2)
>>> co.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 12.566370614359172
现在,让我们利用这个协程来计算圆柱体积:
def cylindrical_volume(r, h):
return circle_area(r) * h
这样显然是不行的,因为调用 circle_area 返回的是一个代表协程的生成器,需要调度它才能获得计算结果。不过没关系,我们可以这么写:
def cylindrical_volume(r, h):
co = circle_area(r)
while True:
try:
yield co.send(None)
except StopIteration as e:
floorage = e.value
return floorage * h
这个是一个协程函数,它先创建一个子协程用于计算底面积,然后用一个永久循环驱动子协程执行。
每次循环时,它先调用 send 方法将执行权交给子协程。如果子协程用 yield 语句归还执行权,这里同样用 yield 将执行权交给调用者,yield 值也一并向上传递。如果子协程退出,它将取出子协程执行结果并完成计算。
>>> co = cylindrical_volume(2, 3)
>>> co.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 37.69911184307752
因此,cylindrical_volume 就像一个中间人,在调用者和子协程之间来回传递执行权。函数调用很常见,如果涉及协程的函数调用都需要用样板代码传递执行权,那简直就是一个噩梦!为此,Python 引入 yield from :
def cylindrical_volume(r, h):
floorage = yield from circle_area(r)
return floorage * h
例子中 yield from 的作用相当于上一例子中的 while 循环,因此这两个例子是完全等价的。与业务逻辑无关的样板代码消除后,新函数变得简洁纯粹,更加清晰易懂了!
# async await
# 协程对象
直接使用生成器实现协程,虽然逻辑上可行,但语义上有点令人摸不着头脑:
>>> co = circle_area(1)
>>> co
<generator object circle_area at 0x10500db50>
为突显协程语义,Python 引入了 async 关键字:
async def circle_area(r):
return math.pi * r ** 2
被 async 关键字标识的函数会被编译成异步函数,调用后得到一个 coroutine 对象:
>>> co = circle_area(1)
>>> co
<coroutine object circle_area at 0x1050f7050>
coroutine 对象与 generator 对象类似,我们可以调用 send 方法来调度 coroutine 对象:
>>> co.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 3.141592653589793
coroutine 对象的语义更加准确,而且我们再也不需要在函数代码中显式编写 yield 语句了,这未免有点画蛇添足。
# 协程执行
青出于蓝而胜于蓝,如果 coroutine 没执行完毕便被意外销毁,Python 将输出警告信息:
>>> co = circle_area(2)
>>> del co
__main__:1: RuntimeWarning: coroutine 'circle_area' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
此外,Python 还引入了 await 关键字,代替前面提到的 yield from 语句。与 yield from 类似,await 将执行权交给子协程,并等待它退出。如果子协程需要暂时归还执行权,await 同样承担起中间人角色,在调用者与子协程间来回接棒。
async def cylindrical_volume(r, h):
floorage = await circle_area(r)
return floorage * h
无须多言,await 的语义也比 yield from 准确。另外,Python 还引入了 可等待对象 ( awaitable )。例子如下:
class Job:
def __await__(self):
print('step 1')
yield
print('step 2')
yield
print('step 3')
return 'coding-fan'
可等待对象需要提供 await 魔术方法,实现成普通生成器即可。然后,await 就可以驱动生成器的执行:
async def do_job(job):
value = await job
print('job is done with value {}'.format(value))
>>> co = do_job(Job())
>>> co.send(None)
step 1
>>> co.send(None)
step 2
>>> co.send(None)
step 3
job is done with value coding-fan
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
如果你觉得可等待对象 Job 难以理解,可以将它想象成等价的 yield from 形式,便豁然开朗了:
def do_job(job):
value = yield from job.__await__()
print('job is done with value {}'.format(value))
await 本无法驱动普通生成器,可等待对象却另辟蹊径,因而它在协程库中有重要作用。