2. 事件循环核心机制

asyncio 的并发能力并非来自线程或进程,而是来自一个单线程内的事件循环(Event Loop)。理解事件循环,等于掌握了 asyncio 的“心脏”:它决定谁先运行、谁被挂起、谁在何时被唤醒。本节从原理出发,逐步落到任务调度、Future 与回调,最后给出调试模式与性能观测的实操方法。

一、事件循环到底在做什么

事件循环本质上是一个“就绪队列 + 定时器堆 + I/O 多路复用”的调度器。它反复执行以下循环:从就绪队列取出回调执行;执行到 await 时若结果未就绪,就把当前协程挂起并注册一个唤醒回调;随后调用 selector(Linux 上通常是 epoll)等待 I/O 事件或最近的定时器到期;一旦有事件就绪,把对应回调重新放回就绪队列。整个过程单线程串行,因此协程之间不会真正并行,只会在 await 处让出控制权。

关键点在于:只有遇到 await 且该 await 的对象尚未完成时,协程才会让出控制权。如果一段代码里没有 await,或者 await 的对象已经完成,事件循环不会被打断,其他任务只能排队等待。这正是“异步代码里混入同步阻塞调用会拖垮整个循环”的根本原因。

事件循环单轮调度结构 就绪队列 callback / Task.step 定时器堆 call_later / sleep I/O 多路复用 selector.select(timeout) 事件循环 取就绪回调 → 执行 遇 await 未完成 → 挂起 等待事件 → 重新入队 Task / Future 状态:PENDING→DONE 回调链 add_done_callback

二、任务调度:Task 与 create_task

协程对象本身不会运行,必须被包装成 Task 并交给事件循环。asyncio.create_task(coro) 会立即把协程注册进循环,返回一个 Task 对象;而 await coro 则是“就地等待”,不会创建独立任务。两者的区别决定了调度行为:

import asyncio

async def worker(name, delay):
    await asyncio.sleep(delay)
    return f"{name} done"

async def main():
    t1 = asyncio.create_task(worker("A", 1))
    t2 = asyncio.create_task(worker("B", 0.5))
    # 两个任务已并发启动,main 继续执行
    print(await t2)   # B done(约 0.5s 后)
    print(await t1)   # A done(约 1s 后)

asyncio.run(main())

这里 t1、t2 在 create_task 时就被排入就绪队列,main 的两次 await 只是等待结果。若改成 await worker("A",1) 再 await worker("B",0.5),总耗时约 1.5 秒,因为第二个协程要等第一个完全结束才开始。这是初学者最常见的性能陷阱。

调度顺序还受“就绪队列 FIFO”影响:同一时刻被唤醒的多个任务,按入队顺序执行。若需要控制并发度,可用 asyncio.Semaphore 限制同时运行的任务数,而不是一次性 create_task 上千个。

三、Future 与回调:底层结果容器

Future 是一个“占位结果”对象,表示某个尚未完成的操作。它有三种状态:PENDING、CANCELLED、FINISHED。Task 是 Future 的子类,额外持有协程。Future 的核心方法是 set_result()、set_exception()、add_done_callback()。当结果被设置时,所有通过 add_done_callback 注册的回调会被调度执行。

loop = asyncio.get_running_loop()
fut = loop.create_future()

def on_done(f):
    print("结果:", f.result())

fut.add_done_callback(on_done)
loop.call_later(0.2, fut.set_result, 42)
# 约 0.2s 后输出:结果: 42

回调在事件循环线程中执行,因此回调里不能做长时间阻塞操作,否则会卡住整个循环。若回调本身是协程,应使用 asyncio.ensure_future 或 create_task 包装,而不是直接调用。现代代码更推荐用 await fut 或 asyncio.wait_for 替代手动回调,可读性和异常处理都更好。

四、调试模式与性能观测

开启调试模式只需在运行入口设置环境变量或调用 API:

import asyncio, logging
logging.basicConfig(level=logging.DEBUG)

asyncio.run(main(), debug=True)
# 或 PYTHONASYNCIODEBUG=1 python app.py

调试模式会带来三类关键信息:一是“协程从未被 await”的警告,帮助发现忘记 await 的 bug;二是慢回调日志,默认超过 100ms 的回调会被记录,暴露阻塞点;三是更详细的异常追踪。生产环境不建议长期开启,因为额外检查会带来性能开销。

性能观测方面,可结合 loop.slow_callback_duration 调整慢回调阈值,用 asyncio.all_tasks() 查看当前存活任务数量,判断是否存在任务泄漏。更系统的观测可接入 asyncio 的调试钩子或第三方工具(如 aiomonitor),实时查看任务栈。一个实用判断依据是:若事件循环单轮耗时持续超过 50ms,通常意味着存在同步阻塞或过重的回调,需要拆分或移入线程池。

实践建议:先用 debug=True 跑一遍测试,确认没有“coroutine was never awaited”警告;再用 loop.slow_callback_duration = 0.05 收紧阈值,定位耗时回调;最后用任务计数确认没有只创建不回收的 Task。