CPython asyncio 概念全景:事件循环、协程、Task 与 await 的底层机制详解 CPython asyncio 概念全景事件循环、协程、Task 与 await 的底层机制详解【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文为 CPython 官方文档 Doc/howto/a-conceptual-overview-of-asyncio.rst 的深化解读围绕asyncio的核心组件——事件循环、协程函数、协程对象、Task、await与 Future——建立一套稳固的心智模型。读完本文你将能回答三个关键问题await一个对象时幕后究竟发生了什么asyncio如何区分不占 CPU 的任务如网络请求与需要 CPU 的任务如计算阶乘以及如何用 Future 亲手实现一个asyncio.sleep。文中所有源码证据均取自当前仓库的 Lib/asyncio 标准库实现。第一部分高层概念事件循环一切皆相对它发生asyncio中的一切都相对于事件循环event loop发生。官方文档将其比喻为乐队指挥它拥有一些被明确授予的权力但更多能力来自乐队成员各个任务的配合与让渡。更技术地说事件循环内部维护着一个待执行的作业jobs集合。有些作业由你直接添加有些则由asyncio间接添加。事件循环从待办列表中取出一个作业并调用它即交出控制权作业运行当它暂停或完成时控制权归还事件循环事件循环再从池中挑选下一个作业。你可以粗略地把作业集合想成一条队列作业被加入、然后逐个处理大体按顺序但不总是如此。这个循环无限重复若没有待执行作业事件循环会休眠以避免空耗 CPU直到 I/O 完成或定时器到期才再次被唤醒。import asyncio # 创建一个事件循环并让它无限循环处理作业 event_loop asyncio.new_event_loop() event_loop.run_forever()协作式调度有一个隐含前提作业必须懂得分享。一个贪婪的作业可以独占控制权让其他作业挨饿——这正是后文await coroutine陷阱的根源。异步函数与协程对象普通的 Python 函数调用即执行函数体def hello_printer(): print( Hi, I am a lowly, simple printer, though I have all I need in life -- \nfresh paper and my dearly beloved octopus partner in crime. )而async def而不是普通def定义的是异步函数协程函数coroutine function——调用它并不会执行函数体而是创建并返回一个协程对象coroutine objectasync def loudmouth_penguin(magic_number: int): print( I am a super special talking penguin. Far cooler than that printer. fBy the way, my lucky number is: {magic_number}. ) loudmouth_penguin(magic_number3) coroutine object loudmouth_penguin at 0x104ed2740术语辨析很关键协程函数与协程对象常被混称为 coroutine。在本文语境中coroutine 特指协程对象即types.CoroutineType的原生协程实例注意协程也可以是collections.abc.Coroutine的实例——这一区分对类型检查type checking有实际意义。协程代表函数的函数体/逻辑它必须被显式启动仅仅创建并不会启动它。协程可以在函数体中的各个点暂停与恢复这种可暂停、可恢复的能力正是异步行为的基石。协程是构建在生成器generator之上的生成器函数是包含yield的函数def get_random_number(): # 这可算不上好的随机数生成器 print(Hi) yield 1 print(Hello) yield 7 print(Howdy) yield 4 ...与协程函数类似调用生成器函数不会执行它而是创建生成器对象。用内建函数next可以把生成器推进到下一个yield——即运行然后暂停 generator get_random_number() next(generator) Hi 1 next(generator) Hello 7源码印证asyncio正是利用生成器的send/throw协议驱动协程的。在 Lib/asyncio/tasks.py 的Task.__step_run_and_handle_result中约 L283-L291任务每前进一步都是通过coro.send(None)或coro.throw(exc)来驱动协程的——我们直接使用send方法因为协程没有__iter__和__next__方法源码注释原话。Task绑定到事件循环的协程粗略地说Task 是绑定到事件循环的协程注意是协程对象不是协程函数。创建 Task 会自动将其调度执行——本质上是向事件循环的待办列表中添加一个运行它的回调。推荐通过asyncio.create_task创建coroutine loudmouth_penguin(magic_number5) # 创建 Task 对象并通过事件循环调度其执行 task asyncio.create_task(coroutine)asyncio会自动把 Task 与当前事件循环关联这是刻意设计的简化否则你得手工追踪事件循环对象并把它传递给每一个想创建任务的协程函数。源码印证asyncio.create_task本身只是一层薄封装见 Lib/asyncio/tasks.pydef create_task(coro, **kwargs): Schedule the execution of a coroutine object in a spawn task. Return a Task object. loop events.get_running_loop() return loop.create_task(coro, **kwargs)而真正的调度发生在Task.__init__中Lib/asyncio/tasks.py除非eager_start且事件循环正在运行此时立即执行__eager_start()否则执行self._loop.call_soon(self.__step, contextself._context)——Task 自身并不会被加入事件循环被加入的只是指向__step的回调。用 asyncio.run 管理事件循环实践中推荐使用asyncio.run它负责管理事件循环并确保给定的协程在程序继续前完成import asyncio async def main(): # 进行各种奇奇怪怪的异步操作…… ... if __name__ __main__: asyncio.run(main()) # 在协程 main() 完成之前程序不会到达下面的 print print(coroutine main() is done!)源码印证asyncio.run底层是 Lib/asyncio/runners.py 中的Runner上下文管理器。Runner.__init__接受debug与loop_factory参数Runner.run()会把传入协程包装成任务self._loop.create_task(coro, contextcontext)并在退出时执行收尾——取消所有挂起任务、关闭异步生成器、关闭默认线程池执行器见 Lib/asyncio/runners.py 的close()方法。Task 被垃圾回收的隐患由于被加入事件循环的只是回调而非 Task 对象本身如果 Task 对象在被事件循环调用前被垃圾回收就可能出问题async def hello(): print(hello!) async def main(): asyncio.create_task(hello()) # 其他运行一段时间并把控制权交还给事件循环的异步指令…… ... asyncio.run(main())由于第 5 行创建的 task 对象没有被任何引用持有它可能在事件循环调用它之前被 GC 掉。当事件循环最终尝试运行该任务时可能发现对象已不存在。另一种情形是协程持有 task 引用但协程本身先于 task 完成——协程退出后局部变量离开作用域同样可能被回收。实际上asyncio与 Python GC 付出了相当努力来避免这种事比如 Lib/asyncio/tasks.py 中Task.__del__会在任务仍为 pending 时被销毁时记录 Task was destroyed but it is pending!但那不是肆意冒险的理由——请始终持有 Task 的强引用如存入列表。await行为取决于对象类型await关键字常见于两种用法await task await coroutine关键在于await的行为取决于被 await 对象的类型。await 一个 Task把控制权交还给事件循环async def plant_a_tree(): dig_the_hole_task asyncio.create_task(dig_the_hole()) await dig_the_hole_task # 其他与种树相关的指令。 ...设想事件循环把控制权交给了plant_a_tree()的开头。协程创建了一个 task 并 await 它。await dig_the_hole_task会做两件事把一个回调用于恢复plant_a_tree()加入dig_the_hole_task的回调列表把控制权交还给事件循环。稍后事件循环把控制权交给dig_the_hole_task任务完成它要做的事任务结束后把它的各个回调加入事件循环——在本案中就是恢复plant_a_tree()。概括来说被 await 的 task 完成后原来的任务/协程会被重新加入事件循环的待办列表等待恢复。这是一个基础而可靠的心智模型实践中的控制权交接稍复杂一些但差别不大。源码印证这条路径在 Lib/asyncio/tasks.py 中清晰可见——当Task一步执行后 yield 出一个 Future 时会执行result.add_done_callback(self.__wakeup, contextself._context)然后设置self._fut_waiter result。被等待的 Future 完成后触发__wakeupL359 起后者再调用self.__step()恢复任务。这与文档await 向 task 的回调列表加入恢复回调的描述一一对应。await 一个协程不会交还控制权与 Task 不同直接await coroutine不会把控制权交还给事件循环它等效于调用一个普通同步函数。若先asyncio.create_task(...)包装再 await才会让出控制权。看这个例子import asyncio async def coro_a(): print(I am coro_a(). Hi!) async def coro_b(): print(I am coro_b(). I sure hope no one hogs the event loop...) async def main(): task_b asyncio.create_task(coro_b()) num_repeats 3 for _ in range(num_repeats): await coro_a() await task_b asyncio.run(main())main()的第一句创建了task_b并调度执行。随后coro_a()被反复直接 await控制权从未交给事件循环所以三次coro_a()的输出全部排在coro_b()之前I am coro_a(). Hi! I am coro_a(). Hi! I am coro_a(). Hi! I am coro_b(). I sure hope no one hogs the event loop...若把await coro_a()改为await asyncio.create_task(coro_a())行为就变了main()在该语句处让出控制权事件循环先调用task_b再调用包装coro_a()的任务然后恢复main()I am coro_b(). I sure hope no one hogs the event loop... I am coro_a(). Hi! I am coro_a(). Hi! I am coro_a(). Hi!这个await coroutine的行为容易坑到很多人它可能无意间从其他任务手中霸占控制权事实上卡住事件循环。可以用asyncio.run(..., debugTrue)开启调试模式来检测此类问题——它会记录任何独占执行超过 100 毫秒的协程见官方文档 Doc/library/asyncio.rst 中的 debug mode 说明。这是一个刻意的设计取舍用一点使用上的概念模糊换性能。每次 await 一个 task控制权都要一路上传到事件循环事件循环又要处理内部状态、执行调度逻辑来恢复下一个作业。单个开销看似很小但在有大量await的大程序中会累积成不可忽略的性能拖累。第二部分底层机制nuts and bolts协程的内在工作方式asyncio利用 Python 的四个构件来回传递控制权coroutine.send(arg)、yield、await调用对象的__await__方法、以及StopIteration。coroutine.send(arg)用于启动或恢复协程。若协程是从暂停处恢复arg作为当初暂停它的yield语句的返回值被送入若是首次使用启动而非恢复arg必须是None。完整示例class Rock: def __await__(self): value_sent_in yield 7 print(fRock.__await__ resuming with value: {value_sent_in}.) return value_sent_in async def main(): print(Beginning coroutine main().) rock Rock() print(Awaiting rock...) value_from_rock await rock print(fCoroutine received value: {value_from_rock} from rock.) return 23 coroutine main() intermediate_result coroutine.send(None) print(fCoroutine paused and returned intermediate value: {intermediate_result}.) print(fResuming coroutine and sending in value: 42.) try: coroutine.send(42) except StopIteration as e: returned_value e.value print(fCoroutine main() finished and provided value: {returned_value}.)逐步解析控制流与值的传递第 16 行coroutine.send(None)首次启动协程执行到第 11 行await rockawait会调用对象的__await__方法而Rock.__await__中的yield 7第 3 行使协程暂停值 7 沿调用链一路向上传播回到第 16 行的调用处成为intermediate_result。await还做了一件特别的事它把收到的yield沿调用链继续传播propagate。Beginning coroutine main(). Awaiting rock... Coroutine paused and returned intermediate value: 7. Resuming coroutine and sending in value: 42. Rock.__await__ resuming with value: 42. Coroutine received value: 42 from rock. Coroutine main() finished and provided value: 23.第 21 行coroutine.send(42)恢复协程它从第 3 行yield处继续value_sent_in即为 42协程结束时抛出StopIteration返回值附在异常的value属性上returned_value即main()的返回值 23。值得注意的两个为什么协程函数里直接yield那样它就成了异步生成器函数async generator function是完全不同的东西。协程函数里yield from一个普通生成器会报SyntaxError: yield from not allowed in a coroutine.。这是刻意为之——只保留使用协程的一条路径换概念上的简洁。事实上yield from与await做的事基本相同。yield最初也被禁止后来为支持异步生成器才被重新允许。因此协程让出控制权yield的唯一方式就是 await 一个其__await__方法内含yield的对象。这一点在 C 层实现中同样成立Task.__step_run_and_handle_result中Lib/asyncio/tasks.py对裸yieldyield 出None的处理是self._loop.call_soon(self.__step, ...)源码注释写明裸 yield 让出控制权一个事件循环迭代yield 出 Future、生成器或其他值则分别触发等待逻辑或RuntimeError。源码印证await Future 的完整闭环Lib/asyncio/futures.py 中Future.__await__会先设置self._asyncio_future_blocking True然后yield self——这正对应文档描述的__await__里yield即让出控制权。随后 Task 侧检测到_asyncio_future_blocking并注册__wakeup回调Lib/asyncio/tasks.pyFuture 完成后__wakeup调用__step()恢复任务恢复时__step再次调用coro.send(None)把控制权送回协程。文档中await task 是向回调列表添加恢复回调的高层描述由此得到完整的底层印证。Future计算状态与结果的代表FutureDoc/library/asyncio.rst 中的 asyncio-future-obj表示一次计算的状态与结果——名字致敬尚未到来之事对象就是盯住那件事的方式。Future 的关键属性状态statepending、cancelled或done之一结果result状态转为 done 时被设置。与协程不同Future不代表要执行的实际计算它代表的是该计算的状态与结果——好比交通信号灯红、黄、绿或指示器。asyncio.Task通过继承asyncio.Future获得这些能力。上一节说task 存有一个回调列表并不完全准确真正实现回调逻辑的是Future类Task只是继承者在 CPython 中为class Task(futures._PyFuture)见 Lib/asyncio/tasks.py而Future的状态机定义在 Lib/asyncio/base_futures.py。Future 也可以脱离 Task 直接使用。Task 在其协程完成时把自己标记为 done而 Future 更灵活——你说它 done 它才 done。这正是让你自定义等待与恢复条件的灵活接口。源码印证Future.set_result()是标记为完成的入口Lib/asyncio/futures.pyTask 则禁止直接调用它Lib/asyncio/tasks.py 中Task.set_result直接raise RuntimeError(Task does not support set_result operation)——Task 只能经由协程的StopIteration来完成。亲手实现一个 asyncio.sleep下面利用 Future 实现一个模拟asyncio.sleep的async_sleep注册若干任务到事件循环然后 await 包装async_sleep(3)的 task要求它在三秒后才完成但不阻止其他任务运行。async def other_work(): print(I like work. Work work.) async def main(): # 向事件循环添加几个其他任务这样异步睡眠时有事可做。 work_tasks [ asyncio.create_task(other_work()), asyncio.create_task(other_work()), asyncio.create_task(other_work()) ] print( Beginning asynchronous sleep at time: f{datetime.datetime.now().strftime(%H:%M:%S)}. ) await asyncio.create_task(async_sleep(3)) print( Done asynchronous sleep at time: f{datetime.datetime.now().strftime(%H:%M:%S)}. ) # asyncio.gather 等效于 await 集合中的每个任务。 await asyncio.gather(*work_tasks)async_sleep用一个 Future 来精确控制任务何时被标记为 done——如果future.set_result()负责把 Future 标记为 done 的方法从未被调用该任务永远不会结束async def async_sleep(seconds: float): future asyncio.Future() time_to_wake time.time() seconds # 把观察者任务加入事件循环。 watcher_task asyncio.create_task(_sleep_watcher(future, time_to_wake)) # 阻塞直到 future 被标记为 done。 await future再配合一个朴素的YieldToEventLoop()对象从其__await__方法中yield从而让出控制权。这等效于调用asyncio.sleep(0)但语义更清晰——毕竟用asyncio.sleep来演示如何实现asyncio.sleep有点作弊class YieldToEventLoop: def __await__(self): yield async def _sleep_watcher(future, time_to_wake): while True: if time.time() time_to_wake: # 这把 future 标记为 done。 future.set_result(None) break else: await YieldToEventLoop()事件循环照常遍历任务给予控制权在它们暂停或完成时收回。watcher_task每完整一轮事件循环被调用一次每次恢复时检查时间不够就再次暂停、归还控制权时间一到_sleep_watcher标记 Future 为 done 并退出while True。由于该辅助任务每轮只被调用一次可以推断这个异步睡眠至少睡三秒而非恰好三秒——asyncio.sleep亦然官方实现基于loop.call_later定时器见 Lib/asyncio/tasks.pyh loop.call_later(delay, futures._set_result_unless_cancelled, future, result)后return await future。完整程序输出$ python custom-async-sleep.py Beginning asynchronous sleep at time: 14:52:22. I like work. Work work. I like work. Work work. I like work. Work work. Done asynchronous sleep at time: 14:52:25.这个实现不必要地绕了吗是的。它的目的不是最优而是用一个简单例子展示Future 的通用性且该模式可被仿照到更复杂的等待条件上。如果不需要外部决定何时完成的能力可以不用 Future 直接写async def simpler_async_sleep(seconds): time_to_wake time.time() seconds while True: if time.time() time_to_wake: return else: await YieldToEventLoop()注意simpler_async_sleep中await YieldToEventLoop()是直接 await 协程外的 awaitable每轮循环都会真正让出控制权因此同样不会阻塞事件循环。心智模型小结把两部分拼起来asyncio的控制权流转可以浓缩为一张表操作是否让出控制权底层机制源码证据await task/await future是向 Future 添加__wakeup回调控制权回到事件循环Lib/asyncio/tasks.pyawait coroutine否等效于同步函数调用直接驱动协程执行create_task(coro)否调度是异步的loop.call_soon(self.__step)入队回调Lib/asyncio/tasks.py协程内await YieldToEventLoop()是裸 yield 触发call_soon(self.__step)Lib/asyncio/tasks.py协程return—抛出StopIteration(value)Task 借此set_resultLib/asyncio/tasks.py这套模型与 Doc/howto/a-conceptual-overview-of-asyncio.rst 的结论一致理解谁在什么时候把控制权交给谁是读懂asyncio推荐模式如用create_taskgather并行、避免裸await coroutine阻塞循环、持有 Task 强引用、必要时开启debugTrue检测独占执行的全部前提。想进一步深入可参阅 Doc/library/asyncio.rst 中asyncio完整 API 的其余章节。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考