首页
/ CPython asyncio 全景解析:用 async/await 编写结构化高并发异步 I/O 应用

CPython asyncio 全景解析:用 async/await 编写结构化高并发异步 I/O 应用

2026-09-06 18:57:58作者:傅爽业Veleda

导读

本文以 CPython 源码树中的官方总览文档 Doc/library/asyncio.rst 为核心骨架,系统梳理 Python 标准库 asyncio(异步 I/O)的完整能力版图:从高层 API(协程与任务、流式网络 I/O、子进程、队列、同步原语)到内省 API(任务调用图、跨进程命令行诊断工具),再到面向库与框架作者的低层 API(事件循环、传输与协议、Future 桥接)。读完本文,你将掌握 asyncio 的模块组织方式、顶层入口(asyncio.run / Runner)、asyncio REPL 的交互姿势、进程级任务诊断命令,以及如何在 CPython 源码与文档中快速定位每个功能对应的实现与参考页面。


一、什么是 asyncio:并发代码的标准库基础

asyncio 是 Python 中一个用来编写并发代码的库,其编程模型建立在 async/await 语法之上。它是当下许多 Python 异步框架的底层地基,这些框架被用于构建高性能网络服务与 Web 服务器、数据库连接库、分布式任务队列等,例如各类 async 生态的 Web 框架与驱动都直接构建在 asyncio 之上。

从设计定位看,asyncio 尤其适合 IO-bound(I/O 密集型)且结构化的网络代码——所谓“结构化”,指代码以清晰、可读的协程与任务组织,而不是散落一地的回调。官方文档(Doc/library/asyncio.rst)对它的定位可概括为三个层次:

层次 面向人群 解决的问题
高层 API 普通应用开发者 并发运行协程并完全掌控其执行、执行网络 I/O 与 IPC、控制子进程、通过队列分发任务、同步并发代码
内省 API 调试 / 诊断 检查当前进程中 Task 与 Future 的异步调用图;用命令行工具检查另一个正在运行的 Python 进程中的任务
低层 API 库与框架开发者 创建与管理事件循环(网络、子进程、OS 信号)、基于传输与协议实现高效协议、桥接回调风格代码到 async/await

这一分层结构直观地体现在 Lib/asyncio/ 包的源码组织上:从 Lib/asyncio/init.py 可以看到,包通过 from .xxx import * 聚合了 base_eventscoroutineseventsfuturesgraphlocksrunnersqueuesstreamssubprocesstaskstaskgroupstimeoutsthreadstransports 等子模块的公开符号(每个子模块以 __all__ 声明导出面)。值得注意的细节是,事件循环实现按平台分流:

# Lib/asyncio/__init__.py
if sys.platform == 'win32':  # pragma: no cover
    from .windows_events import *
    __all__ += windows_events.__all__
else:
    from .unix_events import *  # pragma: no cover
    __all__ += unix_events.__all__

也就是说,Windows 与 Unix 系平台会各自加载适配的事件循环实现,顶层 API 行为保持一致,这正是 asyncio 跨平台抽象的基础。

一个最经典的“Hello World”示例(取自文档侧边栏)即可演示其核心用法:

import asyncio

async def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')

asyncio.run(main())

二、能力全景与文档地图:一个入口,四大文档目录

在 CPython 官方文档中,Doc/library/asyncio.rst 扮演的是 asyncio 的总览与入口页。它通过若干 toctree 将全部专题文档组织为四个分组,读者可按图索骥:

所有实现源码统一位于 Lib/asyncio/,与文档形成一一对应的镜像关系,例如 Runner 文档指向 Lib/asyncio/runners.py,调用图文档指向 Lib/asyncio/graph.py,命令行工具文档指向 Lib/asyncio/tools.py

平台限制提示:官方文档页通过 .. include:: ../includes/wasm-notavail.rst(即 Doc/includes/wasm-notavail.rst)声明 asyncio 的可用性——该模块在 WASI 环境不可用,在其他 WebAssembly 环境下同样不受支持,需要进一步查阅文档中的 wasm-availability 说明。

三、高层 API:开箱即用的并发工具箱

总览文档把高层 API 的能力归纳为五个方向:运行协程、网络 I/O 与 IPC、子进程、任务队列、并发同步。asyncio-api-index.rst 则给出了一份更细粒度的速查索引,下面分类展开。

3.1 协程运行与任务编排(Tasks)

这一组工具负责“把 asyncio 程序跑起来”,并围绕 Task 做调度与并发编排。核心条目如下:

入口 作用
asyncio.run(coro, ...) 创建事件循环、运行协程、最终关闭循环
asyncio.Runner 上下文管理器,简化在同一上下文中多次调用异步函数
asyncio.Task / asyncio.create_task(...) Task 对象 / 创建并启动一个 Task
asyncio.TaskGroup 上下文管理器,可靠地等待组内所有任务完成
asyncio.current_task() / all_tasks() 返回当前 Task / 返回事件循环上尚未完成的所有 Task
await asyncio.sleep(sec) 休眠指定秒数
await asyncio.gather(...) 并发调度并等待多个协程
await asyncio.wait_for(aw, timeout) 带超时地运行
await asyncio.shield(aw) 将协程从取消中“保护”起来
await asyncio.wait(...) 监视多个协程直至完成
asyncio.timeout(delay) 更现代的超时运行方式(替代 wait_for 的部分场景)
asyncio.to_thread(func) 在独立 OS 线程中异步运行普通函数
asyncio.run_coroutine_threadsafe(...) 从另一个 OS 线程调度协程
asyncio.as_completed(...) for 循环监视完成情况

其中 asyncio.run()顶层唯一入口,它在 Lib/asyncio/runners.py 中实现,负责:管理事件循环的生命周期、终结异步生成器(finalize async generators)、关闭默认执行器。其签名与语义(来自 asyncio-runner.rst)值得细读:

asyncio.run(coro, *, debug=None, loop_factory=None)
  • coro 可以是任意 awaitable 对象(3.14 起);不能在同一线程已有运行中的事件循环时再次调用;
  • debug=True/False 显式开关事件循环调试模式,None(默认)则遵循全局的 asyncio debug mode 设置(3.10 起);
  • loop_factory 用于自定义事件循环的创建方式,否则默认使用 asyncio.new_event_loop(),循环在结束时被关闭(3.12 起);
  • 执行器在程序结束时获得 5 分钟的关闭宽限期,超时未完成则发出警告并强制关闭执行器(3.9 起调用 loop.shutdown_default_executor)。

3.2 Runner 上下文管理器与 Ctrl-C 的优雅处理

当需要在同一个事件循环与同一个 contextvars.Context 里依次执行多个顶层异步函数时(典型场景如 IPython、unittest runner、命令行工具等交互式宿主),Runner 比多次调用 asyncio.run 更合适。文档给出了与 run() 等价的写法:

async def main():
    await asyncio.sleep(1)
    print('hello')

with asyncio.Runner() as runner:
    runner.run(main())

Runner惰性初始化的(见 Lib/asyncio/runners.py):构造函数本身不初始化底层结构,内嵌的 loop 与 context 在进入 with 体、或首次调用 run() / get_loop() 时才创建。其状态由 _State 枚举(CREATED → INITIALIZED → CLOSED)显式跟踪。主要方法:

  • run(coro, *, context=None):在内嵌事件循环中执行 awaitable;传入协程会包装成 Task;可用 context 指定自定义的 contextvars.Context
  • close():终结异步生成器、关闭默认执行器、关闭事件循环并释放内嵌 Context;
  • get_loop():返回与 Runner 关联的事件循环。

关于 Ctrl-C(3.11 起):由于 SIGINT 中断 asyncio 内部结构可能导致程序挂起无法退出,asyncio 对 KeyboardInterrupt 做了专门处理。从 asyncio-runner.rst 与 runners.py 源码(Runner.run 中判断主线程且 SIGINT 仍为默认处理器时安装自定义信号处理器)可以还原完整机制:

  1. Runner.run() 在任何用户代码执行前安装自定义 SIGINT 处理器,函数退出时移除;
  2. Runner 为传入协程创建主任务;
  3. 按下 Ctrl-C 后,自定义处理器调用 Task.cancel(),在任务内抛出 CancelledError,使 Python 栈正常展开——开发者可用 try/excepttry/finally 做资源清理;主任务取消后 Runner.run() 再抛出 KeyboardInterrupt
  4. 若用户代码是 Task.cancel() 无法打断的紧循环,第二次 Ctrl-C 会立即抛出 KeyboardInterrupt,不再走任务取消路径。

3.3 流式网络 I/O 与 IPC(Streams)

高层网络编程基于 Stream 抽象,覆盖 TCP 与 Unix socket。总览文档特别注明 streams 组文档中的典型使用场景是 asyncio-example-stream 里的 TCP 客户端/服务端示例。API 速查索引给出以下条目:

入口 作用
await asyncio.open_connection(host, port) 建立 TCP 连接
await asyncio.open_unix_connection(path) 建立 Unix socket 连接
await asyncio.start_server(client_cb, host, port) 启动 TCP 服务端
await asyncio.start_unix_server(client_cb, path) 启动 Unix socket 服务端
asyncio.StreamReader 高层 async/await 接收对象
asyncio.StreamWriter 高层 async/await 发送对象

一个最小化 echo TCP 服务端与客户端组合如下(演示 start_server / open_connection / StreamReader / StreamWriter 的协作方式):

import asyncio

async def handle(reader, writer):
    data = await reader.read(100)
    writer.write(data)
    await writer.drain()
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(handle, '127.0.0.1', 8888)
    async with server:
        await server.serve_forever()

asyncio.run(main())

对服务端/客户端的完整可运行示例,直接查阅 asyncio-stream.rst

3.4 子进程(Subprocesses)

asyncio 允许在事件循环内异步创建子进程、读取输出并与之交互:

入口 作用
await asyncio.create_subprocess_exec(*args) 创建子进程(不经过 shell)
await asyncio.create_subprocess_shell(cmd) 通过 shell 执行命令

对应文档 asyncio-subprocess.rst 中提供 shell 命令执行的完整示例;实现位于 Lib/asyncio/subprocess.py,其底层与事件循环的 subprocess transport 相衔接。

3.5 队列(Queues):任务分发与连接池

队列用于在多个 Task 之间分发工作负载、实现连接池以及 pub/sub 模式:

  • asyncio.Queue —— FIFO 队列
  • asyncio.PriorityQueue —— 优先级队列
  • asyncio.LifoQueue —— 后进先出队列

典型用法(“用队列在多个 worker Task 间分发负载”的完整示例见 asyncio-queue.rstasyncio_example_queue_dist)大致是:生产者协程 put 任务,多个 worker 协程并发 get 处理:

import asyncio
import random

async def worker(name, queue):
    while True:
        item = await queue.get()
        await asyncio.sleep(random.random())   # 模拟处理耗时
        print(f'worker {name} 处理了 {item}')
        queue.task_done()

async def main():
    q = asyncio.Queue()
    workers = [asyncio.create_task(worker(i, q)) for i in range(3)]
    for n in range(10):
        await q.put(n)
    await q.join()           # 等待队列清空
    for w in workers:
        w.cancel()

asyncio.run(main())

3.6 同步原语(Synchronization)

asyncio 提供与 threading 风格一致、但可在 Task 内使用的同步原语(详见 asyncio-sync.rst,实现于 Lib/asyncio/locks.py):

  • asyncio.Lock —— 互斥锁
  • asyncio.Event —— 事件
  • asyncio.Condition —— 条件变量
  • asyncio.Semaphore —— 信号量
  • asyncio.BoundedSemaphore —— 有界信号量
  • asyncio.Barrier —— 屏障

它们与线程原语的关键区别在于:这些原语是 非阻塞的await lock.acquire() 不会阻塞事件循环线程,而是挂起当前 Task。与高层 API 配套的异常还有 asyncio.CancelledError(Task 被取消时抛出)与 asyncio.BrokenBarrierError(Barrier 被破坏时抛出),完整异常清单见 asyncio-exceptions.rst

四、内省 API:看清 Task 与协程的执行全景

从 3.14 起,asyncio 为诊断异步程序提供了两类重磅能力:进程内调用图 API 与跨进程命令行工具。

4.1 进程内调用图:print_call_graph / format_call_graph / capture_call_graph

对应 asyncio-graph.rst,实现位于 Lib/asyncio/graph.py。这些工具可以追踪正在运行的协程、Task 或挂起的 Future 的完整调用图,既可用于程序内部,也可被外部 profiler 与调试器复用:

  • asyncio.print_call_graph(future=None, /, *, file=None, depth=1, limit=None):打印当前任务(或指定 Task/Future)的调用图。条目从栈顶帧开始向下直至调用点;depth 用于在当前任务上跳过栈顶若干帧;limit 控制每条调用栈保留的条数——正数保留最靠近调用点的、负数保留最顶层的、0 表示只打印 “awaited by” 信息而不打印调用栈;file 省略时输出到 sys.stdout
  • asyncio.format_call_graph(...):与 print_call_graph 相同但返回字符串;没有当前任务时返回空串;
  • asyncio.capture_call_graph(...):返回结构化的 FutureCallGraph(future, call_stack, awaited_by) 数据类对象,其中 call_stackFrameCallGraphEntry(frame) 元组、awaited_by 是若干 FutureCallGraph 的元组,便于程序化处理。

文档给出如下示例(任务 test 挂在 TaskGroup 之下时,打印出“谁在被谁 await”的完整链条):

import asyncio

async def test():
    asyncio.print_call_graph()

async def main():
    async with asyncio.TaskGroup() as g:
        g.create_task(test(), name='test')

asyncio.run(main())

输出示意:

* Task(name='test', id=0x1039f0fe0)
+ Call stack:
|   File 't2.py', line 4, in async test()
+ Awaited by:
   * Task(name='Task-1', id=0x103a5e060)
      + Call stack:
      |   File 'taskgroups.py', line 107, in async TaskGroup.__aexit__()
      |   File 't2.py', line 7, in async main()

为了让调用图跨结构保持连通,asyncio 要求 shieldTaskGroup 等控制流结构配合维护关系;当开发者使用 Future.add_done_callback 之类低层 API 手写中间 Future 时,需要手动登记关系,对应两个低层工具函数:

  • asyncio.future_add_to_awaited_by(future, waiter, /):登记 future 正被 waiter 等待(两者必须是 Future/Task 或其子类,否则无效果),随后必须成对调用下面的函数注销;
  • asyncio.future_discard_from_awaited_by(future, waiter, /):登记 future 不再被 waiter 等待。

有趣的是,连 Task 自身的内部唤醒路径都依赖这一机制:在 Lib/asyncio/tasks.pyTask.__wakeup 中可以看到 futures.future_discard_from_awaited_by(future, self) 的调用,说明任务调度器本身也为调用图服务。

4.2 跨进程命令行工具:python -m asyncio ps / pstree

对应 asyncio-tools.rst,实现位于 Lib/asyncio/tools.py。通过模块入口(Lib/asyncio/main.py 中的 argparse 子命令定义),你可以只读地检查另一个正在运行的 Python 进程,无需修改或重启目标进程:

python -m asyncio pstree [--retries N] PID
python -m asyncio ps [--retries N] PID

两个子命令的用途与输出形态:

  • pstree PID:以树状展示任务与协程的层级关系。每个任务显示其完整协程栈,并按“谁 await 了它”嵌套展示,特别适合快速定位任务层级中哪条分支被阻塞、卡在哪个协程栈的哪一行。若 await 图存在环(通常意味着编程错误),命令会报错而非输出树:

    ERROR: await-graph contains cycles - cannot print a tree!
    cycle: Task-2 → Task-3 → Task-2
    
  • ps PID:以扁平表格列出目标进程所有 pending 任务,每行展示事件循环线程 ID(tid)、任务 ID 与名称、协程栈,以及(若有)等待方任务的栈、名称与 ID。与 pstree 不同,即使 await 图含环,ps 也会照常输出全部任务。

  • --retries N:当目标进程在读取状态过程中发生变化导致读取失败时,最多重试 N 次(默认 3,由 __main__.pyargparsedefault=3 可见)。

需要注意的使用前提:该命令只在受支持的平台上可用,且可能需要相应权限才能检查其他进程;它读取目标进程状态时不在其中执行任何代码。文档演示中,先在 A 终端运行一个由两层 TaskGroup 构成的多级任务程序并打印 PID,再在 B 终端执行 python -m asyncio pstree 12345,即可看到从 Task-1 → main → TaskGroup.__aexit__ → TaskGroup._aexit 逐级嵌套、最终到达 play → sleep 的完整调用树——这在诊断“到底哪个协程卡住了”时极其直观。

五、低层 API:为库与框架作者准备的机制

当高层 API 不满足需求时,asyncio 提供事件循环、传输/协议、Future 三类低层设施:

文档中特别将这几项标注为“为库与框架开发者准备”,而普通应用开发者应优先使用第三节的高层 API。

六、asyncio REPL:用顶层 await 交互式实验并发

文档在总览页专门给出 asyncio REPL 用法:直接以模块方式启动一个内置并发上下文交互环境。

$ python -m asyncio
asyncio REPL ...
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> await asyncio.sleep(10, result='hello')
'hello'

其实现位于 Lib/asyncio/main.py。结合源码可以看到几个值得注意的实现细节:

  1. 顶层 awaitAsyncIOInteractiveConsole 通过给编译标志加入 ast.PyCF_ALLOW_TOP_LEVEL_AWAIT 允许用户直接输入 await ...
  2. 线程模型:REPL 交互运行在独立的 REPLThread 中,主线程负责 loop.run_forever(),从而实现输入与事件循环互不阻塞;用户代码中的协程通过 loop.create_task(...) 调度到事件循环线程执行;
  3. 上下文:REPL 预置 contextvars.copy_context(),并通过 loop.call_soon_threadsafe(..., context=self.context) 保持上下文一致;同时把 asyncio 注入到交互命名空间,因此示例中 import asyncio 一行其实已自动完成;
  4. 启动钩子与环境变量:3.13 起若可用则使用 PyREPL 作为交互前端,此时会执行 PYTHONSTARTUP 指向的启动脚本;该 REPL 与 PYTHON_BASIC_REPL 环境变量只提供有限兼容,官方建议普通用途使用默认 REPL(功能完整、特性最新);
  5. 审计事件:REPL 会发出 cpython.run_stdin 审计事件;PYTHONSTARTUP 脚本执行时发出 cpython.run_startup 审计事件。总览文档注明审计事件的引入时间是 3.12.5(同时回溯合入 3.11.10、3.10.15、3.9.20 与 3.8.20)。

此外,该入口同时承载了第四节所述命令行工具:不带子命令参数时进入交互式 REPL,带 ps / pstree 子命令时执行跨进程任务诊断(见 __main__.py 末尾的 match args.command: 分支)。

七、调试模式、指南文档与阅读路线图

官方文档为 asyncio 的学习者与排障者预留了一整套指南:

  • 概念总览a-conceptual-overview-of-asyncio 解释了 asyncio 的基本原理(事件循环、协程、可等待对象等),是理解后续 API 的前置阅读;
  • 开发与调试asyncio-dev.rst 系统讲解调试手段,核心是 Debug Mode(debug 模式)——开启后事件循环会检测诸如“协程从未被 await”“执行耗时过长的回调”“检测到 async 函数被直接调用未 await”等问题。asyncio.runRunnerdebug 参数(True/False/None)均可直接控制该模式,默认 None 表示遵守全局设置(可通过 PYTHONASYNCIODEBUG 环境变量或 loop.set_debug(True) 打开);
  • 线程集成asyncio-threading.rst 讲解 asyncio 与线程混用的规则与工具(to_threadrun_coroutine_threadsafe 等);
  • API 速查:需要快速查找某个高层 API 时用 asyncio-api-index.rst,查找低层 API 时用 asyncio-llapi-index.rst

这套文档体系的组织方式与源码模块严格对应,是开发者日常查阅 API、定位实现的权威入口。

八、小结:按需选择正确的抽象层

回顾总览文档,asyncio 的价值在于用一条主线串起从应用层到框架层的完整异步抽象栈

  • 应用开发者:用高层 API(asyncio.run + TaskGroup/gather + Streams/Subprocess/Queue/Sync)编写结构化、易读的 IO-bound 并发代码;用 REPL(python -m asyncio)交互验证;用调用图与命令行工具(ps / pstree)做运行时诊断;
  • 框架与库作者:深入事件循环、Transport/Protocol、Future 与 Lib/asyncio/base_events.pyLib/asyncio/events.py 等低层实现,理解扩展点与跨平台差异;
  • 排查问题时:优先开启 Debug Mode,结合 asyncio-dev.rst 的检查项定位典型错误。

CPython 当前源码树(本仓库版本号见 Include/patchlevel.h,为 3.16 开发分支 3.16.0a0)中,asyncio 已沉淀出从 run/Runner(3.7/3.11)、TaskGrouptimeout,到 3.14 新增的调用图 API、跨进程 CLI 工具,再到 3.15 的 --retries 选项这一系列能力——这套既有广度又有纵深的抽象体系,正是 Python 异步生态长盛不衰的底层支撑。若想逐项深入,建议沿本文引用的专题文档(Doc/library/asyncio-task.rstasyncio-eventloop.rst 等)与其对应的 Lib/asyncio/ 源码对照阅读。

登录后查看全文
热门项目推荐
相关项目推荐