8.2. 协程与任务¶
协程是构建 asyncio 程序的工作单元;任务则是应用程序并发运行多个协程的方式。
8.2.1. 协程¶
协程是用 async def 声明的函数::
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
其主体看起来就像一个普通函数,只多了一项要素:await。在协程必须等待某事的任何位置——一次休眠、一次网络读取、某个事件被置位——它都会 await 一个知道如何挂起该协程、直到它所等待的东西就绪的表达式。在每一个 await 处,协程都会把控制权交还给 asyncio;一旦被 await 的操作完成,asyncio 会从同一位置恢复它。
asyncio 模块提供了两种休眠:
asyncio.sleep()——参数以秒为单位,接受浮点数。asyncio.sleep_ms()——参数以毫秒为单位,接受整数。这是 MicroPython 的扩展;在摄像头上通常是更合适的选择,因为固件中的各种时序参数都是以毫秒为单位的。
单独一个 async def 本身什么也不做。调用 heartbeat(500) 并不会执行其主体;它返回一个协程对象,这个对象需要由 asyncio 来调度。调度它最简单的方式是 asyncio.run()::
asyncio.run(heartbeat(500))
asyncio.run() 启动事件循环,将交给它的协程作为顶层入口点进行调度,驱动循环直到该协程返回,然后拆除循环。对于单个协程而言,这就是整个程序。对于多个协程,应用程序则要借助任务。
8.2.2. 任务¶
任务是 asyncio 对协程的封装,意思是把它与当前协程并发地调度,并让我继续往下执行。asyncio.create_task() 会创建一个任务,并返回一个表示这项已调度工作的 Task 对象::
task = asyncio.create_task(heartbeat("fast", 100))
该协程现在已进入循环的调度计划;调用方并没有等待它。返回的 Task 就是调用方之后用来与这项正在运行的工作交互的句柄。
一旦应用程序拿到了句柄,就可以用它做三件事:
等待任务完成。
Task本身是可 await 的。result = await task会挂起当前协程,直到task的协程返回,然后以该协程返回的任何值恢复执行(或者重新抛出它所抛出的任何异常)。取消任务。
task.cancel()会安排在任务协程的下一个await处向其内部抛出asyncio.CancelledError,给它一个机会在finally块中运行清理代码。超时与取消 页面会介绍其中的细节。之后识别它。
asyncio.current_task()返回当前正在运行的协程所对应的Task。大多数脚本从不调用它;它会出现在监测代码和异常处理程序中。
脚本并非每次都必须捕获句柄。应用程序启动后任其运行的一次性后台任务可以丢弃返回值——循环仍然会调度它们::
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
这两次 create_task 调用调度了两个心跳,却没有等待其中任何一个。控制权立即返回到 main,随后它 await 一次五秒的休眠。在它休眠期间,两个心跳任务取得进展;循环会在任何一个准备好运行的任务之间轮转。五秒之后 main 返回,循环拆除所有仍然存活的任务,然后 asyncio.run() 返回给调用方。
每当应用程序确实需要上述三种操作之一时,就捕获句柄。实际上这意味着几乎总是要捕获,因为干净地关闭一个应用程序意味着要取消它所衍生的后台任务——取消页面会介绍这一模式。
8.2.3. 两行规则¶
最小的 asyncio 程序就是上面那些示例所结尾的那两行::
async def main():
...
asyncio.run(main())
其他一切——应用程序所创建的任务、用来协调它们的原语、它所打开的流——都发生在 main 内部(以及 main 所衍生的协程内部)。当一个脚本超出了摄像头经典的 while True: csi0.snapshot() 循环的承载能力时,答案并不是在多个地方调用 asyncio.run();而是把新的工作以更多任务的形式折叠进 main 中。