人人都会AI编程

28.5 协程进阶:事件循环原理、异步框架实现机制

更新时间:2026-07-12

掌握了 async/await 基本写法之后,想要真正写出高效的异步程序,或者排查诡异的并发问题,就必须深入理解事件循环异步框架底层的工作方式。这一节用尽量直白的语言讲清楚这两件事。

事件循环的核心原理

事件循环(Event Loop)本质是一个“待办任务调度器”,它持续不断地检查:

  • 有没有已经就绪的 I/O 操作(比如网络响应返回了、文件读完了)?
  • 有没有到时间的定时任务?
  • 有没有被 call_soon 安排立即执行的回调?

事件循环的工作流程可以简化为三步循环:

  1. 收集:从操作系统获取哪些文件描述符(socket)已经可读/可写。
  2. 调度:把对应这些就绪事件的协程/回调拿到执行队列里。
  3. 执行:逐个运行这些协程/回调,直到 await 一个尚未就绪的 Future(或 I/O 操作),这时控制权交回给事件循环;事件循环回到第一步。

为什么这能实现高并发?
因为整个过程中只使用一个线程。等待 I/O 时,这个线程不阻塞,而是去处理别的协程。当 I/O 就绪时,对应的协程又会被唤醒继续执行。这就是经典的“协作式多任务”——协程在自己 await 时主动交出控制权,不会像线程那样随时被系统中断。

一个简化的概念模型

# 伪代码,示意事件循环内部如何工作
event_loop = EventLoop()

async def task_a():
    await socket_recv(sock_a)  # 让循环去等待 sock_a 可读
    process(data)

async def task_b():
    await sleep(2)             # 让循环在 2 秒后唤醒自己
    process(other)

event_loop.create_task(task_a())
event_loop.create_task(task_b())
event_loop.run_forever()       # 启动循环

socket_recv(sock_a) 内部,实际是把 (sock_a, 可读事件) 注册到事件循环,然后 yield(暂停)当前协程,循环去跑 task_b。等 sock_a 真正可读时,循环把 task_a 放回执行队列,从上次暂停点继续执行。

Python 异步框架的实现机制

Python 的异步生态建立在 asyncio 标准库之上,而 asyncio 又建立在生成器和 Future/Task 对象之上。

核心组件拆解:

  1. 协程与生成器

Python 的协程在底层是生成器的“增强版”。await 相当于 yield from,但加入了异步上下文。解释器在碰到 await 时,把当前协程挂起,直到等待的 Future 完成。

  1. Future / Task
  • Future 是一个占位符,代表“未来会得到的结果”。协程 await 一个 Future,本质上就是订阅它的通知。
  • Task 继承自 Future,是事件循环实际调度的最小单元。当你 asyncio.create_task(coro()) 时,事件循环会创建一个 Task,把它放进就绪队列等待调度。
  1. 事件循环实现

asyncio 底层依赖 Selector(或 kqueueepoll)来高效监听多个 I/O 事件。每当循环检查到事件就绪,它会找到关联的 Task,将 Task 移动到“就绪队列”等待执行。

  1. 调度策略

事件循环严格执行“单线程串行调度”。某一时刻只有一个协程在运行,其他要么在等待 I/O,要么在就绪队列排队。await 点就相当于协程主动退让,让其他协程有机会运行。如果某个协程里执行长时间不 await 的计算(CPU 密集操作),整个循环都会被阻塞,其他所有协程都得不到执行。这就是为什么异步编程需要“只在 IO 密集处用 await”,计算密集任务应该丢给线程/进程池。

异步 Web 框架(如 FastAPI / aiohttp)利用了什么?
这些框架在拿到一个 HTTP 请求时,会创建一个 Task 来处理该请求。在请求处理过程中,每当需要查询数据库(异步驱动)、调用外部 API 或读写文件,框架就用 await 挂起当前请求的 Task,让事件循环去处理别的请求。因此一个线程就能同时承载成千上万个连接,内存开销比多线程模型小很多。

几点实用理解:

  • 异步不等于快,而是不阻塞:它擅长提升 IO 密集型场景的吞吐量,而不是单个请求的处理延迟。
  • 混用同步阻塞代码是灾难:如果在协程中调用 time.sleep() 或同步的网络请求库(如 requests),整个事件循环都会被堵住。一定要用 await asyncio.sleep() 和异步化的库(aiohttphttpx 异步模式等)。
  • async/await 传染性:一旦某层用了异步,所有调用这条链的函数都要写成 async def,除非在某个边界将异步任务通过 run_in_executor() 交给线程池隔离。

理解了事件循环的调度逻辑,你就知道协程为什么会“协作”、为什么需要非阻塞依赖,也能在写异步程序时更有把握地避免阻塞、死锁和活跃度问题。