生成器中异步IO的起源
之前,你看到了基于生成器的旧式风格的协程示例,虽然它已经被更为明确的原生协程所取代,但是还是值得回顾一下:
可以实验一下,如果你只纯粹的调用 py34_core()
或者 py35_core()
而不使用 await
, 或者不调用 asyncio.run()
方法或其他高级函数,运行结果会怎样?单独调用一个协程会返回一个协程对象:
这表面上看起来有些无趣,协程调用的结果是一个协程对象。
一个小测验:Python还有哪些功能和这个比较像?(Python的什么功能是自身被调用的时候实际却“没做什么”?)
希望你能想到生成器作为答案,因为协程是在生成器之上做了一些增强扩展,其中还有一些行为是类似的:
事实证明,异步IO的基础是生成器函数(无论是使用 async
还是旧式的 @asyncio.coroutine
声明协程)。从技术上讲,相比于 yield
,await
更接近于 yield from
。(但是有一点要记住,yield from
仅仅是用来替换 for i in x(): yield i
的语法糖。)
为什么生成器适合用来实现异步IO,就是它可以随意的停止和重启。例如,你可以在迭代一个生成器对象时暂停,然后在迭代器剩余的值上恢复迭代。当一个生成器函数运行到 yield
时,它会抛出这个值,然后将处于空闲状态,直到需要抛出新的值。
可以通过一个例子加深理解:
关键字 await
与之行为类似,在协程自身被挂起并告知其他协程开始工作的时候会标记一个断点。这里的”挂起“是指暂时交出控制权限,但却不是完全退出或结束。记住,yield
,以及拓展出的 yield from
和 await
都会在协程执行的时候标记一个断点。
这是函数与生成器之间的根本区别。函数是要么都执行,要么都不执行。函数一旦开始执行,除非是遇到 return
,否则不会停止,然后将值返回并推给调用方(函数的调用者)。与之相对,生成器每当遇到 yield
都会暂停不在运行,它不仅可以将值返回给调用堆栈,还可以在使用 next()
恢复调用时保留其中的局部变量。
生成器的另一个少为人知的重要功能。你可以通过 .send()
向生成器中发送一个值,这就允许生成器(以及协程)调用(await
)其他方法而不会阻塞。我不会深入探究这个功能实现的细节,因为它主要是为了在幕后实现协程,你不需要也不应该直接使用这种方式。
如果你有兴趣了解更多内容,可以看一看PEP 342,Python从PEP 342开始正式引入了协程。Brett Cannon 的Python中async-await工作原理也值得一读,还有PYMOTW对asyncio的评述。最后,还有 David Beazley 的协程与并发的探讨,深入探讨了关于协程运行的机制。
将上面的文章精简成几句话:协程通过一种非常规的机制运行,当协程调用 .send()
时,返回的结果是调用返回时抛出的异常对象的属性。这些还有一些更复杂的细节,但是对于实践却没有什么帮助,我们继续讲下面的。
为了方便总结,这里列出了一些协程作为生成器的关键点:
1. 协程是根据生成器方法的高级特性并重新规划后的方法
2. 旧式的基于生成器的协程使用 yield from
等待协程的结果。新式的Python语法中原生协