技术文章:用 pdb 追踪 asyncio 内部执行机制
来源标题:Internals of Asyncio
一篇技术文章通过 pdb 单步追踪一次具体的 asyncio 执行过程,分析事件循环的就绪队列、字节码指令和调度器决策。文章基于 Python 3.13.3 的 asyncio 标准库,意在解答 await 执行时运行什么字节码、控制流走向何处等问题。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
基于 Python 标准库 asyncio - v13.13.3。
在上一章中,我们通过考察 Gunicorn 服务器的内部结构,开始了对大型代码库结构的讨论。在本章中,我们通过追踪 asyncio 的内部实现来继续这一讨论。
大多数 Python 开发者每天都在使用 asyncio,却不理解它实际是如何工作的。诸如:当 await 执行时,运行的是什么字节码?事件循环的就绪队列里有什么?控制流去了哪里?这些问题都悬而未决。在本章中,我们使用 pdb 追踪一次具体的 asyncio 执行过程,分析所涉及的关键数据结构、字节码指令和调度器决策。
我是在构建一个房产投资市场平台(用于促进房产卖家和买家之间的交易)时学到这些的。我负责设计、选择技术栈以及处理开发工作。
我选择 Django 作为核心框架,同时选用了一系列来自 Django 和 Python 生态系统中可靠且维护良好的第三方库。对于网关接口服务器,考虑到站点的异步特性,我遵循最佳实践,选择了 Uvicorn。
该平台包含预约调度、带内置对话的咨询请求、邮件提醒等功能。我使用了 django-appointments、django-channels 和 django-q 等工具,它们大多在设计时就考虑了异步执行——要么通过任务队列使用多进程,要么使用 asyncio 的单线程事件循环。对于 WebSocket 支持,特别是实时聊天,我使用了 django-channels,并以 Uvicorn 作为 ASGI 服务器。
异步编程推理起来可能很棘手,因为执行不再是线性的。不再是逐步的流程,而是将工作单元分散到各个 worker 上——一些通过多进程并行运行,另一些由 asyncio 在单线程上下文中处理。
在本章中,我们聚焦于 asyncio 本身:它底层如何工作,使用一个最小示例,该示例反映了稍后由 Uvicorn 和 Django 的 ASGI 接口所运用的机制。在整章中,我们大量依赖 pdb 调试器——我们检查变量状态及其相关变化,观察事件循环在转换时调度的任务、任务和 future 的创建,以及清理和收尾。
以下是我们将使用 pdb 追踪的 Python 代码片段:
import asyncio import logging import dis if sys.platform == "win32": asyncio.set_event_loop_policy( asyncio.WindowsSelectorEventLoopPolicy() )
Configure detailed asyncio logging
logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", stream=sys.stdout, )
Enable asyncio debug logging
asyncio_logger = logging.getLogger("asyncio") asyncio_logger.setLevel(logging.DEBUG) async def task_one(): import pdb pdb.set_trace() print("Task One: Starting") await asyncio.sleep(1) print("Task One: Completed") async def task_two(): import pdb pdb.set_trace() print("Task Two: Starting") t1 = asyncio.create_task(task_one(), name="CustomTask-1") await t1 print("Finish second after nested done") print("Task Two: Completed") async def main(): import pdb pdb.set_trace() print("Main: Starting up and waiting on my_coroutine") await task_two() print("Finish last after nested done") print("Main: All tasks complete") if name == "main":
Enable debug mode
asyncio.run(main(), debug=True)
Python 代码执行:
脚本首先检查平台。如果是 Windows,它将事件循环策略设置为 SelectorEvent,因为 Windows 默认使用 ProactorEventLoop。
它设置调试。本质上,这开始了一个 pdb 调试会话,我们将逐步追踪代码。
它定义了 task_one、task_two 和 main 协程。
最后,它以 main 协程作为参数执行 asyncio.run。
🔍 图 2.1:事件循环执行
在逐步进入调试器之前,我们可以形成一个执行假设:
在任何协程能够运行之前,asyncio 必须首先构造并配置其事件循环。这个初始化阶段很重要:它建立运行时环境,创建必要的数据结构,并准备好将管理所有异步执行的系统。在本节中,我们追踪 asyncio.run 函数从入口到循环开始处理任务之前的转换。我们将考察上下文管理器 Runner 是如何创建的,事件循环是如何实例化的,以及在这个起始点初始化了哪些状态变量。
模块 runners.py 定义了我们的入口函数 run,它接受一个协程并继续如下操作:
在执行中的这一点,在函数内部和调试会话中,我们首先通过调用 events.get_running_loop 确认是否有任何事件循环正在运行,它返回 None。正如预期,一个事件循环存在,但它当前没有运行。当前正在执行的事件循环存储在线程局部变量 _running_loop 中。
lib\asyncio\runners.py(197)run() (Pdb) p asyncio.get_event_loop() (Pdb) p events._get_running_loop() None
创建上下文管理器 Runner 并执行其 run 方法。此时我们可能再次检查事件循环及其关联任务,如下面的 pdb 输出所示,因为在调试会话中,我们在所有转换点掌握状态变量是至关重要的:
lib\asyncio\runners.py(92)run() (Pdb) loop = asyncio.get_event_loop() (Pdb) p asyncio.all_tasks(loop) set()
与当前循环关联的任务是一个空集,因为我们还没有创建任何任务。参考上面的 pdb 输出。
在事件循环已初始化但未运行的情况下,asyncio 将 main 协程转换为一个 Task 对象——一个事件循环可以执行的对象。Task 包装一个协程,管理其执行,并将其调度到事件循环。在这里,我们观察如何
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。