Python 事件循环入门

本文面向刚接触 Python 异步编程的开发者,重点回答以下问题:

  • Event Loop 到底是什么?
  • 它与 Coroutine、Task、Future 有什么关系?
  • await 发生时,事件循环具体做了什么?
  • 网络请求完成后,事件循环如何知道应该恢复哪个 Task?
  • 为什么连续两个 await 是顺序执行?
  • 为什么 create_task() 可以并发?
  • Ready Queue、Timer Queue 和 I/O Selector 分别负责什么?
  • 哪些代码会阻塞事件循环?

本文依据当前 Python 3.14.6 官方文档进行说明。Python 官方将 Event Loop 定义为每个 asyncio 应用的核心,它负责运行异步 Task 和 Callback、处理网络 I/O,并管理子进程等能力。对于普通应用,官方建议优先使用 asyncio.run() 等高层 API,而不是手动操作 Event Loop。(Python documentation)


一、Event Loop 是什么

一句话理解:

Event Loop 是一个持续运行的调度器,它负责决定当前应该执行哪个 Task,以及哪些正在等待的 Task 已经可以恢复。

它本身不会替你执行所有业务逻辑,也不会让同步阻塞代码自动变成异步代码。

假设有三个任务:

Task A:等待 HTTP 响应
Task B:等待数据库查询
Task C:正在处理数据

事件循环可能这样工作:

先运行 Task A
→ A 发出 HTTP 请求并开始等待
→ 暂停 A

再运行 Task B
→ B 发出数据库请求并开始等待
→ 暂停 B

再运行 Task C
→ C 处理一段代码
→ C 遇到 await 后暂停

HTTP 响应到达
→ 恢复 A

数据库结果到达
→ 恢复 B

asyncio 使用的是协作式调度。在一个事件循环线程中,同一时刻只运行一个 Task;当前 Task 执行到需要等待的 await 时才会被挂起,事件循环才会运行其他 Task、Callback 或处理 I/O。(Python documentation)


二、Event Loop 不是线程池

初学者常把 Event Loop 理解为“后台有很多线程帮我执行代码”,这是不准确的。

典型情况下:

一个 Event Loop
运行在一个操作系统线程中
同一时刻执行一个 Task

它实现高并发的主要方式不是创建大量线程,而是:

Task 等待 I/O 时暂停
→ 当前线程去运行其他 Task
→ I/O 完成后恢复原 Task

因此,它更像一个只有一名工作人员的调度中心:

工作人员不会同时处理两份文件
但一份文件等待外部审批时
可以先处理另一份文件

Python 官方说明,Event Loop 通常运行在主线程中,并在该线程内执行所有 Callback 和 Task。当某个 Task 正在运行时,同一线程中的其他 Task 无法运行;只有当前 Task 因 await 挂起,事件循环才会执行下一个 Task。(Python documentation)


三、五个核心角色

理解事件循环,必须先区分以下对象。

概念作用
Coroutine Function使用async def 定义的函数
Coroutine Object调用协程函数后得到的对象
Task把 Coroutine 交给事件循环调度执行
Future代表未来某个时刻会得到的结果
Event Loop调度 Task、Callback、Timer 和 I/O

它们之间的关系可以表示为:

async def 函数
      ↓ 调用
Coroutine Object
      ↓ create_task()
Task
      ↓ 执行到 await
等待 Future / Future-like 对象
      ↓ Future 完成
Task 恢复

Python 官方将 Coroutine、Task 和 Future 列为三类主要 awaitable。Task 用于在事件循环中运行协程;Future 是表示异步操作未来结果的低层 awaitable。(Python documentation)


四、Coroutine Function 和 Coroutine Object

先看代码:

async def fetch_data() -> str:
    return "done"

这里:

fetch_data

协程函数

调用它:

coroutine = fetch_data()

得到的是协程对象

重要的是:

调用协程函数不会自动把它调度到事件循环执行。

下面的代码通常不会真正执行 fetch_data

fetch_data()

它只创建了协程对象。如果既不 await,也不将其包装成 Task,Python 通常会产生:

RuntimeWarning: coroutine was never awaited

正确方式之一是:

result = await fetch_data()

另一种是:

task = asyncio.create_task(fetch_data())
result = await task

Python 官方明确说明,简单调用协程函数只会创建协程对象,不会安排它执行。(Python documentation)


五、Task 是什么

Task 可以理解为:

一个已经交给 Event Loop 管理的协程执行实例。

例如:

task = asyncio.create_task(fetch_data())

此时发生的是:

fetch_data()
→ 创建 Coroutine Object
→ create_task() 将它包装成 Task
→ Task 被安排到 Event Loop 中运行

Task 负责不断推进协程:

运行协程
→ 遇到 await
→ 暂停
→ 等待 Future
→ Future 完成
→ 恢复协程
→ 最终保存返回值或异常

Task 是一个 Future-like 对象。它运行 Python 协程,并且可以被 await、取消、查询结果或异常。(Python documentation)


六、Future 是什么

Future 可以理解为一个“未来结果盒子”。

当前:
结果还没准备好

未来:
可能得到一个值
可能得到一个异常
也可能被取消

它的典型状态是:

PENDING
FINISHED

或者:

PENDING
CANCELLED

Future 自身通常不负责执行具体业务代码。它主要表达:

某个异步操作将来会完成,完成后结果会被放在这里。

例如:

future = loop.create_future()

稍后其他代码可能执行:

future.set_result("hello")

等待方可以写:

result = await future

Future 主要用于把底层基于 Callback 的异步代码连接到高层 async / await 代码。普通应用代码通常不需要自己创建 Future。(Python documentation)


七、asyncio.run() 做了什么

大多数独立 Python 程序的入口会这样写:

import asyncio


async def main() -> None:
    print("start")
    await asyncio.sleep(1)
    print("end")


asyncio.run(main())

asyncio.run() 大致负责:

创建 Event Loop
→ 将 main() 作为顶层异步任务运行
→ 持续运行 Event Loop
→ 等待顶层任务完成
→ 清理异步生成器
→ 关闭默认 Executor
→ 关闭 Event Loop

它应作为 asyncio 程序的主要入口使用,通常只调用一次。同一线程中如果已经有 Event Loop 正在运行,就不能再次调用 asyncio.run()。(Python documentation)

在 FastAPI、Jupyter Notebook 或其他异步框架中,框架本身通常已经启动了 Event Loop,因此业务代码一般直接使用:

await some_coroutine()

而不是再次调用:

asyncio.run(...)

八、Event Loop 内部主要管理什么

为了帮助理解,可以把 Event Loop 看成管理四类东西。

Event Loop
├── Ready Queue
├── Timer / Scheduled Queue
├── I/O Registration
└── Future Completion Callbacks

下面逐个解释。


1. Ready Queue:就绪队列

Ready Queue 存放:

现在已经可以执行的 Callback 或 Task 步骤。

例如:

loop.call_soon(callback)

会将 Callback 安排到下一轮 Event Loop 中执行。

当某个 Future 完成,需要恢复等待它的 Task 时,用于恢复 Task 的 Callback 也会进入 Ready Queue。

在 CPython 的默认 Event Loop 实现中,内部有一个 _ready 双端队列;call_soon() 注册的 Callback 会按注册顺序执行,并且每个 Callback 执行一次。这里的 _ready 属于实现细节,不应在业务代码中直接访问。(Python documentation)


2. Timer Queue:定时任务队列

Timer Queue 保存:

未来某个时间才可以执行的 Callback。

例如:

loop.call_later(5, callback)

表示大约五秒后执行 callback

或者:

loop.call_at(deadline, callback)

表示在 Event Loop 内部时钟到达指定时间时执行。

这些 API 返回 TimerHandle,可以用于取消尚未执行的定时 Callback。Event Loop 使用单调时钟追踪时间,从而避免系统墙上时间被修改造成明显干扰。(Python documentation)

asyncio.sleep() 底层也会利用事件循环的定时机制。官方明确说明,asyncio.sleep() 总是会挂起当前 Task,让其他 Task 获得运行机会。(Python documentation)


3. I/O Registration:I/O 等待登记

当程序等待网络数据时,例如:

data = await reader.read(1024)

事件循环不会反复执行:

数据到了吗?
数据到了吗?
数据到了吗?

更常见的做法是:

向操作系统登记:
这个 Socket 可读时通知我

Unix 平台通常通过 Selector 机制完成。Selector 可以同时监视多个文件描述符,并等待其中某些对象变为可读或可写。select(timeout) 会等待文件对象就绪或超时时间到达,然后返回已就绪对象。(Python documentation)

平台底层可能使用:

Linux:epoll
macOS / BSD:kqueue
其他 Unix:poll / select

业务代码不需要直接操作这些机制,asyncio 会通过 Event Loop 和网络库进行封装。


4. Future Completion Callbacks:完成回调

当一个 Task 执行:

result = await future

future 还没有完成时,Task 会:

在 Future 上登记:
完成以后请唤醒我

然后当前 Task 挂起

Future 支持:

future.add_done_callback(callback)

当 Future 完成时,其 Callback 不会立即在当前调用栈中直接执行,而是通过 loop.call_soon() 安排到事件循环中。(Python documentation)

这正是 Task 被恢复的关键机制。


九、Event Loop 的“一轮”是怎样运行的

可以把一次 Event Loop 迭代理解为下面的过程。

需要注意,这是一种便于理解的模型;不同 Event Loop 实现可能在细节和顺序上有所差异。

1. 查看当前有哪些 Callback / Task 已经就绪
2. 计算下一次定时任务还有多久到期
3. 根据情况等待或轮询操作系统 I/O
4. 把已完成的 I/O 事件转换成 Callback
5. 把已经到期的 Timer 移入就绪队列
6. 执行本轮就绪队列中的 Callback 和 Task 步骤
7. Callback 可能使 Future 完成
8. Future 完成后安排等待它的 Task 被唤醒
9. 进入下一轮

在 CPython 默认基于 Selector 的 Event Loop 中,如果 Ready Queue 已经有任务,I/O 轮询通常使用零超时,也就是不会长时间等待;如果没有立即可运行的工作,Event Loop 可以等待到最近的定时器到期或某个 I/O 事件就绪。官方事件循环实现同时维护立即 Callback、延迟 Callback、I/O 多路复用和任务调度能力。(Python documentation)

可以画成:

                  ┌─────────────────────┐
                  │    Ready Queue      │
                  └──────────┬──────────┘
                     执行 Task / Callback
                    遇到未完成的 await
                      Task 暂时挂起
          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
      等待 Timer         等待网络 I/O        等待其他 Future
          │                  │                  │
          └──────────────────┴──────────────────┘
                       Future 完成
                   唤醒 Callback 进入 Ready
                             └────→ 下一轮继续执行

十、await 发生时到底做了什么

看下面代码:

result = await operation()

不能把它简单理解为:

Event Loop 去执行 operation()

更准确的过程是:

1. 当前 Task 调用 operation()
2. operation() 返回一个 awaitable
3. 当前 Task 开始驱动这个 awaitable
4. 如果 awaitable 可以立即完成,直接得到结果
5. 如果还需要等待,当前 Task 挂起
6. Event Loop 运行其他 Task
7. 等待对象完成后,当前 Task 恢复
8. await 表达式产生返回值或抛出异常

需要强调:

await 不一定每次都会导致任务切换。

如果等待对象已经完成,await 可能立即返回。

不过:

await asyncio.sleep(...)

总会挂起当前 Task,让其他任务获得执行机会。(Python documentation)


十一、Event Loop 如何知道 awaitable 已经完成

这是理解异步调度最关键的问题。

假设:

result = await future

当 Future 尚未完成时:

Task A
→ 等待 Future F
→ 在 F 上登记唤醒 Callback
→ Task A 挂起

稍后某个事件让 Future 完成:

future.set_result(value)

或者:

future.set_exception(error)

或者:

future.cancel()

Future 完成后:

Future F 变成 Done
→ 安排 Done Callback
→ 唤醒 Task A 的 Callback 进入 Ready Queue
→ Event Loop 之后取出这个 Callback
→ Task A 恢复
→ await 返回 value 或抛出异常

Python 官方说明,当 Task 等待 Future 时,Task 会暂停协程执行;Future 完成后,被包装的协程会恢复。Future 的完成 Callback 会通过 call_soon() 进入 Event Loop 调度,而不是同步立即执行。(Python documentation)


网络 I/O 完成时

对于网络请求,过程可能是:

Task A 发起 Socket 读取
→ 事件循环向操作系统登记可读事件
→ Task A 挂起
→ 操作系统收到网络数据
→ 通知 Selector / IOCP
→ Event Loop 处理 I/O 事件
→ 网络库读取数据
→ Future.set_result(data)
→ Task A 被重新放入 Ready Queue
→ Task A 恢复

SelectorEventLoop 使用 selectors 模块监控 I/O 就绪;Windows 的 ProactorEventLoop 使用 I/O Completion Ports。(Python documentation)


定时器到期时

对于:

await asyncio.sleep(5)

可以理解为:

创建一个五秒后到期的定时事件
→ 当前 Task 挂起
→ Event Loop 运行其他任务
→ 五秒后 Timer 到期
→ 对应 Future 完成
→ 当前 Task 进入 Ready Queue
→ 恢复执行

工作线程完成时

对于:

result = await asyncio.to_thread(blocking_function)

过程是:

blocking_function 在线程池运行
→ 当前 Task 等待 asyncio 对应的 Future
→ 工作线程执行完成
→ 通过线程安全机制通知 Event Loop
→ Future 完成
→ 当前 Task 恢复

从其他线程向 Event Loop 安排 Callback 时,需要使用 loop.call_soon_threadsafe();从其他线程提交协程则使用 asyncio.run_coroutine_threadsafe()。(Python documentation)


十二、为什么连续两个 await 是顺序执行

看下面代码:

result_a = await fetch_a()
result_b = await fetch_b()

很多初学者会问:

A 在等待时,Event Loop 为什么不直接执行下一行的 B?

原因是:

第二行仍属于同一个当前 Task,而当前 Task 已经整体暂停在第一行。

时间线如下:

main Task 开始
→ 调用 fetch_a()
→ 等待 A
→ main Task 挂起

Event Loop 可以运行其他已经存在的 Task
但 fetch_b() 还没有被调用
也没有代表 B 的 Task

A 完成
→ main Task 恢复
→ 执行下一行
→ 调用 fetch_b()

Event Loop 不会阅读后面的源码并自动将下一行拆成新 Task。

因此:

await fetch_a()
await fetch_b()

是顺序控制流。

Python 官方示例也展示了连续等待两个协程时,它们依次完成;要并发运行,需要使用 create_task()TaskGroup。(Python documentation)


十三、为什么 create_task() 可以并发

task_a = asyncio.create_task(fetch_a())
task_b = asyncio.create_task(fetch_b())

result_a = await task_a
result_b = await task_b

这里已经创建了两个独立 Task:

Task A
Task B

当主 Task 执行:

await task_a

并被挂起后,Event Loop 仍然可以运行:

Task A
Task B

因此它们可以在各自等待 I/O 时交替推进。

假设 A 和 B 各自等待两秒:

连续直接 await:
约 4 秒

先 create_task 再等待:
约 2 秒

这是并发,不一定是多核并行。Task 用于将协程安排到 Event Loop 中并发运行。(Python documentation)

对于一组生命周期相关的任务,Python 3.11+ 更推荐使用 TaskGroup

import asyncio


async def main() -> None:
    async with asyncio.TaskGroup() as group:
        task_a = group.create_task(fetch_a())
        task_b = group.create_task(fetch_b())

    result_a = task_a.result()
    result_b = task_b.result()

退出 TaskGroup 时会自动等待组内任务;如果其中一个任务以非取消异常失败,其余相关任务会被取消并等待结束。(Python documentation)


十四、Callback 与 Coroutine 有什么区别

Callback 通常是普通同步函数:

def callback() -> None:
    print("called")

可以这样安排:

loop.call_soon(callback)

Callback 被 Event Loop 调用后,会同步执行,直到:

函数返回
抛出异常
或者发生同步阻塞

普通 Callback 内不能直接使用:

await ...

因为它不是协程。

协程则需要创建 Task:

asyncio.create_task(async_function())

可以简单区分:

Callback
→ 普通函数
→ Event Loop 直接调用

Coroutine
→ 可暂停的异步执行体
→ 需要 Task 驱动

call_soon() 用于安排 Callback;create_task() 用于安排 Coroutine。(Python documentation)


十五、Handle 和 TimerHandle 是什么

当你调用:

handle = loop.call_soon(callback)

会得到一个 Handle

它代表:

一个已经被 Event Loop 安排执行的 Callback。

可以取消:

handle.cancel()

调用:

timer_handle = loop.call_later(5, callback)

则得到 TimerHandle,它除了可以取消,还保存预定执行时间。

普通开发者通常很少直接使用这些低层对象,但理解它们有助于理解 Ready Queue 和 Timer Queue。(Python documentation)


十六、什么代码会阻塞 Event Loop

下面代码即使放进 async def,仍然会阻塞:

import time


async def bad() -> None:
    time.sleep(5)

原因是:

time.sleep()
→ 阻塞当前线程
→ Event Loop 所在线程也被阻塞
→ 所有其他 Task 无法运行

同样危险的还有:

requests.get(url)

同步数据库驱动:

cursor.execute(...)

大型 CPU 计算:

for i in range(1_000_000_000):
    ...

Python 官方明确指出,阻塞代码不应直接运行在 Event Loop 线程中;哪怕一秒钟的 CPU 计算,也会让所有并发 Task 和 I/O 操作延迟一秒。(Python documentation)


正确的异步等待

async def good() -> None:
    await asyncio.sleep(5)

这里挂起的是当前 Task,而不是整个线程。


同步阻塞 I/O 的桥接

result = await asyncio.to_thread(blocking_function)

asyncio.to_thread() 会在单独线程中运行同步函数,并返回一个可以等待其结果的协程。它主要适用于那些否则会阻塞 Event Loop 的 I/O 型同步函数。(Python documentation)


CPU 密集代码

纯 Python CPU 密集计算通常不适合简单依赖 to_thread() 获得多核加速。可以考虑:

ProcessPoolExecutor
InterpreterPoolExecutor
独立 Worker
专门计算服务

Python 官方建议通过线程、独立解释器或进程 Executor 隔离会阻塞 Event Loop 的计算。(Python documentation)


十七、Event Loop 与线程的关系

常见模型是:

主线程
└── Event Loop
    ├── Task A
    ├── Task B
    └── Task C

几乎所有 asyncio 对象都不是线程安全的。

从其他线程通知 Event Loop:

loop.call_soon_threadsafe(callback)

从其他线程提交协程:

future = asyncio.run_coroutine_threadsafe(coroutine, loop)

不要从工作线程直接调用普通的:

loop.call_soon(...)

因为它不是线程安全的。(Python documentation)


十八、Unix 和 Windows 的 Event Loop 有什么不同

Python 目前提供两种主要 Event Loop 实现:

SelectorEventLoop
ProactorEventLoop

在 Unix 上,asyncio.EventLoop 指向 SelectorEventLoop,它建立在 selectors 模块之上。

在 Windows 上,asyncio.EventLoop 指向 ProactorEventLoop,后者使用 I/O Completion Ports。

这两种实现底层机制不同,但对大部分应用代码提供相同的高层 async / await 编程模型。(Python documentation)

因此,应用开发者通常不需要关心:

epoll
kqueue
IOCP

但框架和网络库开发者可能需要处理平台差异。


十九、取消和超时也是事件循环调度的一部分

调用:

task.cancel()

不会立刻强行杀死 Task。

它会请求在 Task 中抛出:

asyncio.CancelledError

协程通常会在下一个适合恢复的位置接收到这个异常,从而执行清理代码:

async def worker() -> None:
    resource = await acquire_resource()

    try:
        await do_work(resource)
    finally:
        await release_resource(resource)

Task 取消、Future 取消和超时最终也会通过 Future 状态变化、Callback 和 Event Loop 调度机制传播。(Python documentation)

推荐的超时方式:

async def call_api() -> str:
    try:
        async with asyncio.timeout(10):
            return await request_api()
    except TimeoutError:
        return "timeout"

超时上下文会取消超期 Task,并将相应的取消转换为可在上下文外捕获的 TimeoutError。(Python documentation)


二十、Event Loop 并不保证绝对公平

Event Loop 使用协作式调度。

如果某个 Task 写了长时间的纯同步循环:

async def bad_worker() -> None:
    while True:
        do_cpu_work()

即使它由 async def 定义,只要没有等待点,它就可能长时间占据 Event Loop。

某些计算型异步函数可以偶尔使用:

await asyncio.sleep(0)

主动提供调度机会。官方说明,sleep(0) 提供了一个优化路径,可以让其他 Task 运行。(Python documentation)

但这并不是解决 CPU 密集任务的主要方法。CPU 工作过重时,仍应移出 Event Loop。


二十一、如何调试 Event Loop 问题

可以开启 Debug Mode:

asyncio.run(main(), debug=True)

也可以设置环境变量:

PYTHONASYNCIODEBUG=1

Debug Mode 可以帮助发现:

未 await 的 Coroutine
未读取的 Task 异常
错误线程调用
执行过慢的 Callback 或 Task 步骤
未关闭的资源

Python 官方提供多种方式启用 Debug Mode,并建议在开发阶段配合 asyncio 日志和 ResourceWarning 使用。(Python documentation)

还可以观察当前 Task:

current = asyncio.current_task()
all_tasks = asyncio.all_tasks()

这些 API 可以查看当前正在运行的 Task 和 Event Loop 中尚未完成的 Task。(Python documentation)


二十二、AI 应用中的实际例子

1. 同时调用多个独立工具

import asyncio


async def collect_context(query: str) -> tuple[dict, dict]:
    async with asyncio.TaskGroup() as group:
        web_task = group.create_task(search_web(query))
        db_task = group.create_task(search_database(query))

    return web_task.result(), db_task.result()

这里 Web 搜索和数据库查询互不依赖,可以并发等待。


2. 有依赖关系的 Agent 步骤

plan = await create_plan(user_input)
tool_result = await execute_tool(plan)
answer = await generate_answer(tool_result)

这里必须顺序执行,因为后一步依赖前一步结果。

Event Loop 并不会因为代码是异步的,就自动推断业务依赖或并发关系。


3. LLM 流式输出

async def stream_answer(prompt: str):
    async for token in llm_client.stream(prompt):
        yield token

等待下一个 Token 时,当前 Task 可以挂起,Event Loop 继续处理其他请求。


4. 同步解析器

async def parse_document(path: str) -> list[str]:
    return await asyncio.to_thread(sync_parser.parse, path)

如果解析工作主要是同步磁盘 I/O,这种桥接可以避免阻塞 Event Loop;如果主要是重 CPU 解析,则更适合独立进程或 Worker。


二十三、初学者最佳实践

1. 程序入口优先使用 asyncio.run()

asyncio.run(main())

不要为了“更底层”而手动创建和关闭 Event Loop。官方也建议普通应用优先使用高层 API。(Python documentation)

2. 相关并发任务优先使用 TaskGroup

async with asyncio.TaskGroup() as group:
    ...

这比创建一堆无人管理的后台 Task 更安全。(Python documentation)

3. 不要在 Event Loop 中运行同步阻塞代码

time.sleep(...)
requests.get(...)
大量 CPU 循环

4. 不要认为 async def 自动意味着非阻塞

函数内部调用什么 API,才决定它是否会阻塞。

5. 不要认为 await 自动产生并发

await a()
await b()

仍然是顺序执行。

6. 给外部调用设置超时

async with asyncio.timeout(10):
    ...

7. 限制并发数量

semaphore = asyncio.Semaphore(10)

否则可能压垮:

LLM API
数据库
HTTP Tool
下游服务

8. 开发环境开启 Debug Mode

它能尽早发现未等待 Coroutine 和未处理 Task 异常。(Python documentation)


二十四、常见误区

“Event Loop 同时执行很多 Task”

不准确。

同一 Event Loop 线程在同一时刻通常只执行一个 Task;多个 Task 在等待点之间交替推进。

“Event Loop 会自动执行下一行”

错误。

当前 Task 挂起后,它的下一行也会暂停。只有其他独立 Task 才能运行。

“Future 会自己执行工作”

错误。

Future 通常只代表结果和状态;真正让它完成的是网络回调、定时器、其他 Task、线程或底层库。

“只要写 await 就不会阻塞”

错误。

await time.sleep(5)

会先同步阻塞五秒,再因为 None 不能被 await 而报错。

“Task 就是线程”

错误。

Task 是 Event Loop 中调度协程的逻辑单位,通常仍运行在同一个线程中。

“异步适合所有程序”

错误。

异步主要适合大量 I/O 等待。简单同步脚本或 CPU 密集计算不一定从 Event Loop 中获益。


总结

可以用下面这条主线记住 Python Event Loop:

asyncio.run()
→ 启动并管理 Event Loop

Coroutine
→ 描述异步代码

Task
→ 将 Coroutine 交给 Event Loop 调度

Task 执行到 await
→ 等待 Future
→ Task 挂起

Event Loop
→ 运行其他 Ready Task
→ 等待 Timer 或 I/O

I/O / Timer / Thread 完成
→ Future 变成 Done
→ 唤醒 Callback 进入 Ready Queue

Event Loop 下一轮
→ 恢复原 Task
→ await 得到结果

最核心的一句话是:

事件循环不是在多个 Task 之间随意跳转,而是在一个 Task 主动因等待而暂停后,从已经就绪的其他 Task 中选择下一个执行;等待条件完成时,再通过 Future 的完成回调把原 Task 放回就绪队列。