最近通过的 PEP-0492 为 Python 3.5 在处理协程时增加了一些特殊的语法新功能中很大一部分在 3.5 之前的版本就已经有了, 不过之前的语法并不算最好的, 因为生成器和协程的概念本身就有点容易混淆 PEP-0492 通过使用 async 关键字显式的对生成器和协程做了区分
本文旨在说明这些新的机制在底层是如何工作的如果你只是对怎么使用这些功能感兴趣, 那我建议你可以忽略这篇文章, 而是去看一下内置的 asyncio 模块的文档如果你对底层的概念感兴趣, 关心这些底层功能如何能构建你自己的 asyncio 模块, 那你会发现本文会有有意思
本文中我们会完全放弃任何异步 I/O 方法, 而只限于使用多协程的交互下面是两个很小的函数:
def coro1():
我们从两个最简单的函数开始, coro1 和 coro2 我们可以按顺序来执行这两个函数:
coro1()
我们得到期望的输出结果:
C1: Start
不过, 基于某些原因, 我们可能会期望这些代码交互运行普通的函数做不到这点, 所以我们把这些函数转换成携程:
async def coro1():
通过新的 async 关键字的魔法, 这些函数不再是函数了, 现在它们变成了协程 (更准确的说是本地协程函数) 普通函数被调用的时候, 函数体会被执行, 但是在调用协程函数的时候, 函数体并不会被执行, 你得到的是一个协程对象:
c1 = coro1()
输出:
- <coroutine object coro1 at 0x10ea60990> <coroutine object coro2 at 0x10ea60a40>
- (解释器还会打印一些运行时的警告信息, 先忽略掉)
那么, 为什么要有一个协程对象? 代码到底如何执行? 执行协程的一种方式是使用 await 表达式 (使用新的 await 关键字) 你可能会想, 可以这样来做:
await c1
不过, 你肯定会失望了 await 表达式只有在本地协程函数里才是有效的你必须这样做:
async def main():
接下来问题来了, main 函数又是如何开始执行的呢?
关键之处是协程确实是与 Python 的生成器非常相似, 也都有一个 send 方法我们可以通过调用 send 方法来启动一个协程的执行
c1.send(None)
这样我们的第一个协程终于可以执行完成了, 不过我们也得到了一个讨厌的 StopIteration 异常:
C1: Start
StopIteration 异常是一种标记生成器 (或者像这里的协程) 执行结束的机制虽然这是一个异常, 但是确实是我们期望的! 我们可以用适当的 try-catch 代码将其包起来, 这样就可以避免错误提示接下来我们让我们的第二个协程也执行起来:
try:
现在我们得到了全部的输出, 不过有点让人失望的是这跟最初的输出结果没有啥区别因此我们增加了不少代码, 不过还没有做到交替执行协程与线程相似的地方是多个线程之间也可以交替执行, 不过与线程不同之处在于协程之间的切换是显式的, 而线程是隐式的 (大多数情况下是更好的方式) 所以我们需要加入显式切换的代码
通常生成器的 send 方法会一直运行, 直到通过 yield 关键字放弃执行, 也许你认为我们的 coro1 可以改成这个样子:
async def coro1():
但是我们不能在协程里使用 yield 作为替换, 我们可以使用新的 await 表达式来暂停协程的执行, 直到 awaitable 执行结束于是我们需要的代码类似于 await _something_; 问题是这里 _something_ 是什么呢? 我们必须 await 某个东西, 而不是空! 这个 PEP 解释了什么是可以 await 的 (awaitable) 其中一种是另一个本地协程, 不过这个对我们了解底层细节没有啥帮助另一种是通过特定 CPython API 定义的对象, 不过我们暂时还不打算引入扩展模块, 而只限于使用纯 Python 除此之外, 还剩下两种选择: 基于生成器的协程对象, 或者一个特殊的类似 Future 的对象
接下来, 我们会选择基于生成器的协程对象基本上一个 Python 的生成器 (例如: 某个有 yield 表达式的函数) 可以通过 types.coroutine 装饰被标记成一个协程所以, 这是一个最简单的例子:
@types.coroutine
这定义了一个基于生成器的协程函数要得到基于生成器的协程对象, 只需要执行这个函数我们可以把我们的 coro1 协程修改成下面这样:
async def coro1():
通过上面的修改, 我们期望 coro1 和 coro2 可以交错执行到目前为止, 输出是这样的:
C1: Start
我没看到正如期望的, 在第一条打印语句之后, coro1 停止执行, coro2 接着执行实际上, 我们可以通过下面的代码查看协程对象是如何暂停执行的:
print("c1 suspended at: {}:{}".format(c1.gi_frame.f_code.co_filename, c1.gi_frame.f_lineno))
这可以打印 await 表达式所在的行(注意: 打印的是最外层的 await, 所以这里只是起示例作用, 通常情况下用处不大)
现在的问题是, 如何让 coro1 继续执行完呢? 我们可以再调用一次 send, 代码如下:
try:
得到的输出跟预期一样:
C1: Start
目前, 我们通过为不同的协程显式调用 send 来让它们都执行结束通常情况下这种方式不是很好我们希望的是有一个函数来控制所有的协程的运行, 直到全部协程都执行完成换句话说, 我们期望连续不断的调用 send, 驱动不同的协程去执行, 直到 send 抛出 StopIteration 异常
为此我们新建一个函数, 这个函数传入一个协程列表, 函数执行这些协程直到全部结束我们现在要做的就是调用这个函数
def run(coros):
这段代码每次从协程列表里取一个协程执行, 如果捕获到 StopIteration 异常, 就把这个协程从队列里去掉
接下来我们把手工调用 send 的代码去掉, 代码如下:
c1 = coro1()
综上所述, 在 Python 3.5, 我们现在可以通过新的 await 和 async 功能很轻松的执行协程本文的相关代码可以在 github 上找到
来源: https://juejin.im/entry/5a9d031f518825556e5d8e48