Python asyncio 核心剖析:coroutine、Task、Future 到底有什么区别?
在学习 Python 异步编程时,很多开发者都经历过这样一个阶段:
async 和 await 会写了,asyncio.create_task() 也会用了,甚至已经能写出并发请求程序,但只要继续追问几个问题——
async def foo():
return 42
x = foo()
此时,foo() 到底执行了吗?
再看:
task = asyncio.create_task(foo())
这一行执行之后,foo() 是立即运行了,还是仅仅被放进某个队列?
coroutine、Task、Future 又分别是什么?
为什么它们都能被 await?
如果这些问题还不能非常确定地回答,那么你对 asyncio 的理解可能仍停留在“会用 API”的阶段,而没有真正进入 Python 异步运行机制的内部。
这篇 Python 教程就把这几个概念彻底拆开。
一、先回答最核心的问题:foo() 执行了吗?
先看代码:
async def foo():
print("foo 开始执行")
return 42
x = foo()
print(x)
运行后,你会看到类似:
<coroutine object foo at 0x...>
但你不会看到:
foo 开始执行
原因非常重要:
调用普通函数,会执行函数体;调用
async def定义的协程函数,通常只是创建一个 coroutine object。
因此:
x = foo()
本质上可以理解为:
foo
↓
协程函数 coroutine function
↓ 调用 foo()
x
↓
协程对象 coroutine object
此时它只是一个“等待执行的异步计算过程”。
代码:
print("foo 开始执行")
return 42
还没有真正运行。
可以使用 inspect 验证:
import inspect
async def foo():
return 42
x = foo()
print(inspect.iscoroutinefunction(foo))
print(inspect.iscoroutine(x))
结果:
True
True
这里必须建立一个非常重要的概念:
async def foo():
...
定义的是协程函数。
而:
foo()
得到的是协程对象。
二者不是同一个东西。
二、那么协程对象什么时候真正执行?
最常见的方法是:
await foo()
例如:
import asyncio
async def foo():
print("foo 开始")
await asyncio.sleep(1)
print("foo 结束")
return 42
async def main():
result = await foo()
print(result)
asyncio.run(main())
输出:
foo 开始
foo 结束
42
执行:
await foo()
时,foo() 返回协程对象,而 await 开始驱动这个协程。
这里有一个特别容易被忽略的细节:
await foo()并不会自动创建一个独立 Task。
它更接近:
当前 Task
│
└──进入 foo 协程
│
├──执行代码
│
├──遇到 await
│
└──暂时挂起
也就是说,如果:
async def main():
a = await foo()
b = await bar()
通常执行顺序是:
foo
↓
完成
↓
bar
↓
完成
它仍然具有明显的顺序性。
这也是很多初学者第一次写 asyncio 时遇到的问题:
result1 = await request_a()
result2 = await request_b()
看起来用了两个 await,却没有实现真正意义上的并发调度。
三、asyncio.create_task(foo()) 又发生了什么?
现在来看文章最重要的第二行代码:
task = asyncio.create_task(foo())
它实际上完成了两件事情:
foo()
│
├──创建 coroutine object
│
↓
asyncio.create_task(...)
│
├──把 coroutine 包装成 Task
│
└──交给 Event Loop 调度
Python 官方文档对 create_task() 的定义就是:将 coroutine 包装进 Task 并调度执行。当前官方文档还明确说明,create_task() 使用当前正在运行的事件循环,因此如果当前线程中没有运行中的 event loop,会抛出 RuntimeError。(Python documentation)
所以正确写法通常是:
import asyncio
async def foo():
return 42
async def main():
task = asyncio.create_task(foo())
result = await task
print(result)
asyncio.run(main())
而不能简单地在普通模块顶层写:
task = asyncio.create_task(foo())
因为此时通常不存在“正在运行的事件循环”。
四、创建 Task 之后,协程是不是立即执行?
这是 asyncio 中一个很容易回答错的问题。
看一个实验:
import asyncio
async def foo():
print("1. foo 开始")
await asyncio.sleep(1)
print("2. foo 结束")
return 42
async def main():
print("A")
task = asyncio.create_task(foo())
print("B")
result = await task
print("C", result)
asyncio.run(main())
在传统、默认的调度方式下,你通常会观察到:
A
B
1. foo 开始
2. foo 结束
C 42
为什么不是:
A
1. foo 开始
B
...
因为:
asyncio.create_task(foo())
首先完成的是:
创建 coroutine
→ 创建 Task
→ 把 Task 交给事件循环调度
当前正在运行的 main() 还没有主动把控制权交还给事件循环。
等执行到:
await task
或者其他会真正产生挂起的 await 时,事件循环重新取得控制权,才有机会运行刚刚创建的 Task。
因此可以记住一句非常实用的话:
create_task()的核心含义不是“立即调用函数”,而是“创建一个由事件循环负责推进的异步任务”。
五、用一个实验看清调度时机
下面这个 Python 实战示例尤其值得亲手运行:
import asyncio
async def foo():
print("foo: 开始")
await asyncio.sleep(1)
print("foo: 结束")
async def main():
print("main: 创建任务前")
task = asyncio.create_task(foo())
print("main: 创建任务后")
await asyncio.sleep(0)
print("main: 再次获得执行权")
await task
asyncio.run(main())
典型过程可以理解为:
main 开始
│
├──create_task(foo)
│ │
│ └──foo 被登记为可运行任务
│
├──main 继续
│
├──await asyncio.sleep(0)
│
└──main 交还控制权
│
↓
Event Loop
│
↓
运行 foo
这里的:
await asyncio.sleep(0)
经常被用于主动给事件循环一次调度其他任务的机会。
这也揭示了 asyncio 的核心:
asyncio 不是多个 Python Task 同时抢 CPU,而是一种协作式调度。
官方文档明确指出,事件循环一次运行一个 Task;当当前 Task 等待 Future 时,事件循环才能运行其他 Task、callback 或处理 I/O。(Python documentation)
所以 asyncio 的强项是:
HTTP 请求
数据库
Socket
WebSocket
文件/网络 I/O
消息队列
大量等待型任务
而不是直接让一段 CPU 密集代码自动获得多核并行能力。
六、coroutine 到底是什么?
可以把 coroutine 理解成:
一段可以暂停、保存执行状态,并在未来继续执行的计算过程。
普通函数:
def add():
a = 1
b = 2
return a + b
通常执行模型是:
进入
↓
连续执行
↓
返回
↓
结束
而协程:
async def download():
data = await request()
return data
更像:
开始
↓
执行
↓
遇到 await
↓
保存当前状态
↓
暂停
↓
Event Loop 运行其他任务
↓
等待条件完成
↓
恢复
↓
继续执行
因此 coroutine 最重要的能力不是“快”,而是:
可以暂停和恢复。
正是这个能力,让一个线程可以高效管理成百上千个 I/O 任务。
七、Task 又是什么?
如果 coroutine 是“可以执行的异步流程”,那么 Task 就是:
被 Event Loop 管理起来的 coroutine 执行单元。
例如:
coro = foo()
task = asyncio.create_task(coro)
关系是:
Coroutine
│
↓
Task
│
↓
Event Loop
Task 会负责推进 coroutine。
当 coroutine:
return 42
Task 就进入完成状态,并保存结果:
42
因此:
task = asyncio.create_task(foo())
result = await task
本质上就是:
创建 Task
↓
Event Loop 推进 coroutine
↓
coroutine return 42
↓
Task 保存 42
↓
await task 得到 42
你还可以检查 Task 的状态:
task.done()
task.cancelled()
task.result()
task.exception()
例如:
async def main():
task = asyncio.create_task(foo())
print(task.done())
result = await task
print(task.done())
print(task.result())
可能输出:
False
True
42
八、Future 又是什么?
Future 是三者中最抽象、也最容易让初学者困惑的概念。
可以把 Future 理解为:
“未来某个时刻才会有结果”的占位对象。
例如现在我要一个整数,但这个整数还没计算出来:
现在:
Future
result = ???
一秒以后:
Future
result = 42
Future 本身并不代表“如何计算 42”。
它只负责表达:
这个结果现在没有,
以后会有。
举一个底层一些的例子:
import asyncio
async def main():
loop = asyncio.get_running_loop()
future = loop.create_future()
loop.call_later(
1,
future.set_result,
42
)
print("等待结果...")
result = await future
print("结果:", result)
asyncio.run(main())
执行过程:
Future 创建
│
├──状态:Pending
│
├──等待 1 秒
│
├──set_result(42)
│
├──状态:Done
│
└──await Future 得到 42
官方文档将 Future 定位为连接低层 callback 风格代码与高层 async/await 代码的重要桥梁。实际业务代码中通常不需要频繁手动创建 Future。(Python documentation)
九、Task 与 Future 到底是什么关系?
这是理解 asyncio 的关键。
官方 asyncio 实现中:
Task继承了Future的绝大部分接口,但不能像普通 Future 那样从外部直接调用set_result()或set_exception()。(Python documentation)
可以把它理解成:
Future
│
│ 表示“未来结果”
│
└── Task
│
└── Future + coroutine 执行管理
换句话说:
Future 关心结果。
Coroutine 关心计算过程。
Task 把计算过程运行起来,并最终产生 Future 式的结果。
这是最值得记住的一组定义。
十、一张表彻底分清三者
| 概念 | Coroutine | Task | Future |
|---|---|---|---|
| 核心含义 | 可暂停恢复的计算过程 | 被 Event Loop 调度的协程 | 未来结果的占位符 |
能否 await | 可以 | 可以 | 可以 |
| 是否主动调度协程 | 否 | 是 | 通常否 |
| 是否保存最终结果 | 间接 | 是 | 是 |
| 常见产生方式 | foo() | create_task(foo()) | loop.create_future() |
| 应用层常用程度 | 很高 | 很高 | 较低 |
| 典型用途 | 描述异步操作 | 并发运行异步操作 | 对接底层异步机制 |
如果只记一句:
Coroutine = 要做什么
Task = 安排它去做
Future = 将来得到什么
虽然这不是严格的语言规范定义,但对于建立直觉非常有效。
十一、await foo() 与 create_task(foo()) 的本质区别
假设:
async def download(name, delay):
print(name, "开始")
await asyncio.sleep(delay)
print(name, "结束")
写成:
async def main():
await download("A", 2)
await download("B", 2)
大约需要:
A 2秒
+
B 2秒
=
4秒
因为第二个 coroutine 要等第一个执行完成后才开始。
如果改为:
async def main():
task1 = asyncio.create_task(
download("A", 2)
)
task2 = asyncio.create_task(
download("B", 2)
)
await task1
await task2
两个 Task 可以在各自等待 I/O 时交替运行:
0s A 开始
0s B 开始
A 等待
B 等待
2s A 结束
2s B 结束
整体约两秒。
这才是:
asyncio.create_task()
真正重要的价值。
它不是让单个函数执行更快,而是:
让多个“等待很多、真正占用 CPU 时间较少”的任务能够重叠进行。
十二、一个容易犯错的写法
很多人会写:
async def main():
await foo()
await bar()
await baz()
然后疑惑:
“为什么用了 asyncio 还是很慢?”
因为这本质上仍然是:
foo 完成
↓
bar 完成
↓
baz 完成
如果三者互相独立,应考虑:
async def main():
t1 = asyncio.create_task(foo())
t2 = asyncio.create_task(bar())
t3 = asyncio.create_task(baz())
await t1
await t2
await t3
或者使用:
results = await asyncio.gather(
foo(),
bar(),
baz()
)
再或者,在现代 Python 项目中,对于一组生命周期相关的并发任务,更推荐考虑结构化并发:
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(foo())
t2 = tg.create_task(bar())
t3 = tg.create_task(baz())
print(t1.result())
TaskGroup 可以更系统地管理任务生命周期和异常传播,官方文档也把它作为管理相关任务的重要方式。(Python documentation)
十三、千万不要把 Task 当线程
这是另一个高频误区。
执行:
asyncio.create_task(foo())
并不意味着:
启动一个新线程
也不是:
启动一个新进程
而是:
Event Loop
│
├──Task A
├──Task B
├──Task C
│
└──谁遇到 await 等待
↓
就先运行别人
因此下面这种代码即使写成 async:
async def bad():
total = 0
for i in range(10**10):
total += i
return total
它仍然可能长时间阻塞 Event Loop。
因为循环内部根本没有机会交出控制权。
这说明一个极其重要的 Python 最佳实践:
I/O 密集型
↓
asyncio 很适合
CPU 密集型
↓
考虑多进程、线程池、ProcessPoolExecutor、
原生扩展、NumPy 等其他方案
十四、Task 的异常为什么不能不管?
考虑:
async def foo():
await asyncio.sleep(1)
raise ValueError("出错了")
async def main():
asyncio.create_task(foo())
await asyncio.sleep(2)
asyncio.run(main())
你可能遇到:
Task exception was never retrieved
因为 Task 失败了,却没有代码去获取它的异常。
更可靠的写法:
async def main():
task = asyncio.create_task(foo())
try:
await task
except ValueError as exc:
print("捕获异常:", exc)
对于一批相关任务,TaskGroup 往往比随手创建大量“无人管理”的后台 Task 更安全。
Python 官方文档还特别提醒:Event Loop 对 Task 只保留弱引用,因此所谓“fire-and-forget”任务如果确实需要长期存在,应保存强引用;对于成组任务,优先考虑 TaskGroup。(Python documentation)
十五、Task 如何取消?
Task 还有一个重要能力:
task.cancel()
例如:
import asyncio
async def worker():
try:
print("开始工作")
await asyncio.sleep(10)
print("工作完成")
finally:
print("清理资源")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(1)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("任务已经取消")
asyncio.run(main())
典型输出:
开始工作
清理资源
任务已经取消
因此在真实项目中:
try:
...
finally:
...
对于数据库连接、锁、Socket、临时文件等资源的清理尤其重要。
十六、Python 3.14 以后必须知道的 eager_start
前面我们说:
task = asyncio.create_task(foo())
通常是“创建并安排执行”,而不是保证 foo 的函数体一定在这一行同步执行。
但现代 Python 又增加了一层高级机制。
截至 Python 3.14,asyncio.create_task() 已支持:
asyncio.create_task(
coro,
eager_start=...
)
官方文档说明,eager_start 可以控制 Task 是否在创建过程中就开始执行;如果开启 eager execution,协程甚至可以立即运行到第一次真正阻塞的位置。(Python documentation)
此外,Python 3.12 起已经提供:
asyncio.eager_task_factory
使协程能够在 Task 构造阶段同步开始运行。官方也特别提醒,这会改变任务执行顺序等语义。(Python documentation)
所以更严谨的表述应该是:
在传统和常见的默认 Task 调度语义下,
create_task()将协程包装为 Task 并交给 Event Loop,通常需要当前代码把控制权交还给事件循环后,新 Task 才获得执行机会;如果启用了 eager task 机制,则可能在创建阶段立即开始运行。
对于入门学习,先理解传统模型即可。
但对于资深开发者排查:
任务为什么比预期更早运行?
为什么升级 Python 后任务顺序发生变化?
就必须意识到 eager task 的存在。
十七、从底层关系再看一次
现在可以把整个 asyncio 模型画成这样:
async def foo()
│
│ 调用
↓
Coroutine Object
│
│ create_task()
↓
Task
│
│ Event Loop 调度
↓
Coroutine 开始执行
│
│ await I/O
↓
Task 暂停
│
↓
Event Loop 运行其他 Task
│
↓
等待的 Future 完成
│
↓
Task 恢复
│
↓
Coroutine return 42
│
↓
Task = Done
result = 42
从这个图也能发现:
Future
经常隐藏在底层 I/O 等待机制中。
例如一个 Task 正在:
await something
当 something 对应的未来事件没有完成时,Task 暂停。
事件完成以后:
Future Done
↓
唤醒 Task
↓
Task 继续推进 coroutine
这就是 asyncio 最核心的运行循环。
十八、把三者想象成一家餐厅
如果技术定义还是比较抽象,可以换成一个生活化模型。
假设你去餐厅点牛排。
Coroutine:菜谱与做菜过程
准备食材
↓
煎牛排
↓
等待
↓
摆盘
↓
完成
它描述“事情怎么做”。
Task:后厨订单
服务员把:
做一份牛排
真正交给厨房调度。
于是这件事成为一个正在被管理的任务。
Future:取餐凭证
现在菜还没做好,但你拿到一个东西:
订单 9527
状态:制作中
结果:???
以后:
订单 9527
状态:完成
结果:牛排
因此:
Coroutine → 工作流程
Task → 被调度执行的工作
Future → 将来结果的容器
而 Event Loop 就像厨房里的调度系统:
牛排正在煎,需要等
→ 去处理沙拉
沙拉正在烤,需要等
→ 去处理咖啡
牛排时间到了
→ 回来继续处理牛排
一个工作人员也可以高效管理很多“主要时间花在等待”的订单。
这就是异步 I/O 高效的本质。
十九、五道自测题
如果你已经理解本文,可以尝试直接回答下面几个问题。
1. foo() 会执行 foo 函数体吗?
async def foo():
print("hello")
x = foo()
答案:
通常不会。
这里只是创建 coroutine object。
2. 谁真正推进 coroutine?
可以是:
await foo()
也可以通过:
asyncio.create_task(foo())
让 Task 在 Event Loop 中推进它。
3. await foo() 会自动创建独立 Task 吗?
不会。
它通常是在当前 Task 中进入并推进 foo 协程。
4. create_task(foo()) 会创建线程吗?
不会。
Task 是 Event Loop 的异步调度单位,不等于 OS Thread。
5. Task 与 Future 什么关系?
一句话:
Task 可以理解成一个会主动推进 coroutine 的特殊 Future。
二十、最终总结
Python asyncio 看起来有很多名词:
async
await
coroutine
Task
Future
Event Loop
TaskGroup
gather
但真正抓住核心后,整个体系其实非常统一。
请记住下面这条主线:
async def
↓
定义 coroutine function
foo()
↓
创建 coroutine object
await foo()
↓
在当前异步执行流中推进 coroutine
create_task(foo())
↓
把 coroutine 包装成 Task
并交给 Event Loop 调度
Future
↓
表示一个未来才会完成的结果
如果一定要把这篇文章浓缩成三句话,那么就是:
Coroutine 描述“一个可以暂停和恢复的计算过程”。
Task 负责“让这个 coroutine 被事件循环真正调度起来”。
Future 表示“这个异步操作未来会得到的结果”。
而对于文章开头的问题:
async def foo():
return 42
x = foo()
此时:
foo 的函数体通常还没有执行;
x 是 coroutine object。
再执行:
task = asyncio.create_task(foo())
则意味着:
创建 coroutine
↓
包装成 Task
↓
提交给当前 Event Loop
↓
由 Event Loop 调度执行
↓
最终得到 42
一旦真正理解这条链路,你会发现 asyncio 不再是一套需要死记 API 的工具,而是一套逻辑非常优雅的协作式调度模型。
这也是学习 Python 编程过程中一个很有意思的转折点:
一开始,我们关心的是“代码怎么写”。
再往后,我们开始关心:
这一行代码究竟在什么时候执行?
谁在推进它?
它为什么暂停?
又是谁把它唤醒?
当你开始提出这些问题时,你已经从“使用 Python”逐渐走向“理解 Python”。
参考资料
Python 官方 Coroutines and Tasks 文档:Python Coroutines and Tasks
Python 官方 Future 文档:Python Futures
Python 官方 Event Loop 文档:Python Event Loop
进一步学习时,可以重点研究:
asyncio.gather()
asyncio.TaskGroup
asyncio.wait()
asyncio.wait_for()
asyncio.timeout()
asyncio.shield()
Task.cancel()
contextvars
Semaphore
Queue
run_in_executor()
asyncio.to_thread()
如果你正在进行 Python 实战项目,不妨思考两个问题:
第一,你项目中的 await 究竟是在“等待”,还是无意中把本可并发的任务串行化了?
第二,你创建的每一个 Task,最终是否都有明确的生命周期、异常处理与取消策略?
能回答这两个问题,往往比单纯记住几十个 asyncio API 更重要。

404

被折叠的 条评论
为什么被折叠?



