2.32. 协程

协程(coroutine)是一种可以在执行过程中暂停、随后从中断处恢复执行的函数,并且其所有局部变量都保持完好。它与生成器模式如出一辙——一个可以挂起和恢复的函数体——区别仅在于由谁来驱动每一次恢复。

Python 中编写协程的语法是 async / await 这对关键字。async 将函数标记为协程;await 标记函数内部允许暂停的位置。

2.32.1. 定义协程

async def 定义的函数是协程函数。调用它并不会运行函数体,而是返回一个尚未启动的协程对象

async def greet(name):
    print("hello,", name)

coro = greet("Alice")        # body NOT run yet

协程对象暂停在函数的最开始处。必须有某种东西来驱动它才能让函数体运行——这个东西就是事件循环,一个运行时组件。MicroPython 在 asyncio 模块中提供了一个。目前,可以把协程看作“已准备好运行,正等待驱动器”。

2.32.2. 在协程内部暂停

在协程内部,await 表达式会挂起执行,直到被等待的值就绪:

async def fetch_and_log():
    data = await read_sensor()
    print("got:", data)

当函数体执行到 await read_sensor() 时,协程会把控制权交还给正在运行它的东西。当 read_sensor() 完成后,驱动器会在下一行恢复协程,并将结果绑定到 data

await 只在协程内部有效。在普通函数中使用它会导致语法错误。

2.32.3. 与生成器的关系

协程和生成器共享同一套底层机制。区别在于由谁来拉动每一次恢复:

  • 生成器产出的是;消费者用 next() 或通过迭代来拉取下一个值。

  • 协程产出的是控制权;当被等待的操作就绪时,事件循环会调度其恢复。

如果你能理解生成器的产出(yield)这套握手机制,那么协程的握手机制是同样的思路——只不过由事件循环来驱动,而不是由 for 循环来驱动。

事件循环(event loop)是一个小型调度器,它维护着一份正在等待某些事件(定时器、网络事件、另一个协程完成等)的协程列表。每一轮迭代,它都会挑选一个等待条件已满足的协程,将其恢复运行直到下一个 await,然后记录该协程此刻正在等待什么,再转向另一个就绪的协程。其结果是许多任务在单个线程上并发地推进——每个协程在其 await 处自愿让出控制权,而事件循环则用其他任何已就绪、可以推进的协程来填补这些间隙。

在底层,awaityield 使用的是同一套 Python 运行时特性来挂起和恢复函数。关键字之所以不同,是因为围绕它们的约定不同:yield 把一个值交还给用 next() 拉取的消费者;await 则把控制权交给事件循环,由它在被等待的操作就绪时调度恢复。async / await 本质上是协程模式更新的语法——较早的库直接在生成器机制之上构建协程,使用 yield from(在 迭代器与生成器 中介绍)在协程之间委托挂起。

2.32.4. 协程需要驱动器

协程在没有运行时驱动的情况下是惰性的。定义一个协程没有问题;但运行它需要一个事件循环。MicroPython 的 asyncio 模块提供了这个事件循环。Asyncio 一节介绍了如何启动事件循环、在其上调度协程、用锁和事件在协程之间共享状态、处理取消和超时,以及如何围绕这里介绍的 async / await 关键字来构建一个真正的应用程序。