Python asyncio 核心剖析:coroutine、Task、Future 到底有什么区别?

Python asyncio 核心剖析:coroutine、Task、Future 到底有什么区别?

在学习 Python 异步编程时,很多开发者都经历过这样一个阶段:

asyncawait 会写了,asyncio.create_task() 也会用了,甚至已经能写出并发请求程序,但只要继续追问几个问题——

async def foo():
    return 42

x = foo()

此时,foo() 到底执行了吗?

再看:

task = asyncio.create_task(foo())

这一行执行之后,foo() 是立即运行了,还是仅仅被放进某个队列?

coroutineTaskFuture 又分别是什么?

为什么它们都能被 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 式的结果。

这是最值得记住的一组定义。


十、一张表彻底分清三者

概念CoroutineTaskFuture
核心含义可暂停恢复的计算过程被 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 更重要。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值