本文带大家用自顶向下的方式理解 Python 协程。看完你能回答这三个问题:

  1. async defawait 到底是什么?
  2. create_task 和直接 await 有什么区别?
  3. 怎么让三个任务同时跑、哪个先好就先处理哪个?

一、为什么需要协程?

想象你在厨房里做饭:

  • 同步(一个人做饭):红烧肉焯水的时候你干等着,水开了才去切菜
  • 多线程(请了三个厨师):三个厨师各做各的,但厨房太小容易撞在一起,还要协调谁用哪个灶
  • 协程(一个厨师 + 眼观六路):红烧肉焯水的时候你去切菜,水开了回来处理——一个人把所有事安排得明明白白

协程的核心理念就是:在等待 I/O 的时候去做别的事

二、基础语法

2.1 async def 和 await

1
2
3
4
5
6
7
8
import asyncio


async def say_greeting(name: str, delay: int):
print(f"{name} 说:先等 {delay} 秒...")
await asyncio.sleep(delay) # 在这里"暂停",让出控制权
print(f"{name} 说:等了 {delay} 秒,我回来了!")
return f"{name} 完成"

关键理解:

  • async def 定义一个协程函数,调用它不会执行,而是返回一个协程对象
  • await 后面必须跟一个可等待对象(协程 / Task / Future)
  • await 的意思是”在这里暂停,等它完事我再继续”
1
2
coro = say_greeting("虚幻", 2)   # 只是得到一个协程对象,还没跑
print(type(coro)) # <class 'coroutine'>

2.2 入口:asyncio.run()

1
2
3
4
5
6
async def main():
result = await say_greeting("塑梦", 1)
print(result)

if __name__ == "__main__":
asyncio.run(main()) # 唯一入口

asyncio.run() 会创建一个事件循环,跑完自动关闭。

2.3 协程对象 vs 协程函数

1
2
3
4
5
async def hello():    # 协程函数
return "你好"

print(hello) # <function hello at 0x...> ← 协程函数
print(hello()) # <coroutine object hello at 0x...> ← 协程对象

协程函数是定义,协程对象是调用定义得到的暂停的函数快照。事件循环只能调度协程对象。

三、并发:Task 与 create_task

3.1 直接 await 是串行的

1
2
3
4
# 总耗时:3 + 1 + 2 = 6 秒
r1 = await say_greeting("A", 3)
r2 = await say_greeting("B", 1)
r3 = await say_greeting("C", 2)

3.2 create_task 让它们同时跑

1
2
3
4
5
# 总耗时:3 秒(最慢的那个)
t1 = asyncio.create_task(say_greeting("A", 3))
t2 = asyncio.create_task(say_greeting("B", 1))
t3 = asyncio.create_task(say_greeting("C", 2))
r1, r2, r3 = await t1, await t2, await t3

create_task 把协程包装成 Task 对象并注册到事件循环。Task 会被事件循环”记住”,当某个 await 让出控制权后,事件循环开始调度所有注册的 Task。

重要create_task 只是注册,不是立即执行。真正触发执行的是第一个 await 让出控制权的时候

1
2
3
4
5
6
async def main():
asyncio.create_task(say("你好")) # 注册了
asyncio.create_task(say("我好困")) # 注册了
# 没有 await!main() 直接结束
# → asyncio.run() 关闭事件循环
# → 两个 Task 还没开始就被杀了 😱

3.3 协程对象 vs Task 对象

协程对象 Task 对象
怎么得到 调用 async def 函数 asyncio.create_task(coro)
类型 <class 'coroutine'> <class '_asyncio.Task'>
被事件循环跟踪
查看状态 ✅(done() / cancelled()
取结果 ✅(result()

四、并发工具:gather 与 wait

4.1 gather — 等全部完成

1
2
3
4
5
6
results = await asyncio.gather(
say_greeting("A", 3),
say_greeting("B", 1),
say_greeting("C", 2),
)
# results = ['A 结果', 'B 结果', 'C 结果'](顺序与传参一致)

等价于手动 create_task + 分别 await,但更安全(异常时自动取消其他 Task)。

4.2 wait — 精细控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tasks = [
asyncio.create_task(say_greeting("X", 4)),
asyncio.create_task(say_greeting("Y", 2)),
asyncio.create_task(say_greeting("Z", 3)),
]

# 等全部完成(默认)
done, pending = await asyncio.wait(tasks)

# 等最快那个
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

# 等第一个出错
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)

return_when 有三种选项:

选项 含义
ALL_COMPLETED 等所有 Task 完成(默认)
FIRST_COMPLETED 等最快那个完成就返回
FIRST_EXCEPTION 等第一个抛出异常,没异常则等全部

4.3 小心 pending

wait 返回时,pending 里的 Task 还在后台继续跑。如果不想要它们了:

1
2
3
done, pending = await asyncio.wait(tasks, return_when=FIRST_COMPLETED)
for t in pending:
t.cancel()

五、Future:底层的”盒子”

Future 是一个”未来会有结果”的盒子。Task 是 Future 的子类,日常开发中你几乎不直接碰 Future。

1
2
3
4
5
6
f = asyncio.Future()
print(f.done()) # False

f.set_result("你好")
print(f.done()) # True
print(f.result()) # "你好"

每一个 await 的背后,最终都是通过 Future 来传递结果的。

六、实战:三道菜

学了这么多,来一个完整的例子。你有三个炉灶,可以同时做菜:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"""
题目:三道菜

你有三个炉灶,可以同时做菜:
- 红烧肉:需要 5 秒
- 清炒时蔬:需要 2 秒
- 番茄蛋汤:需要 3 秒

要求:
1. 三个菜同时开始做(并发)
2. 哪个菜先做好就先端上桌(打印 "XXX 做好了!")
3. 全部做好后打印 "开饭啦!"
4. 记录总耗时
"""

import asyncio
import time


async def cook(name: str, duration: int):
await asyncio.sleep(duration)
return name


async def main():
start = time.perf_counter()
meals = [["红烧肉", 5], ["清炒时蔬", 2], ["番茄蛋汤", 3]]
tasks = [asyncio.create_task(cook(name, t)) for name, t in meals]
while tasks:
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
tasks = list(pending)
for t in done:
print(f"{t.result()} 做好了!")
print("开饭啦!")
print(f"总耗时 {time.perf_counter() - start:.2f} 秒")


if __name__ == "__main__":
asyncio.run(main())

运行结果:

1
2
3
4
5
清炒时蔬 做好了!
番茄蛋汤 做好了!
红烧肉 做好了!
开饭啦!
总耗时 5.00 秒

三个菜同时开始,清炒时蔬 2 秒最快上桌,番茄蛋汤 3 秒,红烧肉 5 秒。总耗时等于最慢的那个菜——这就是并发的威力。

七、总结

1
2
3
协程函数 (async def) → 协程对象 → create_task → Task 对象 → 注册到事件循环

await 让出控制权 → 事件循环调度
  • 协程 = 暂停的函数快照,需要事件循环来调度
  • create_task 注册await 让出控制权,事件循环调度
  • 并发 = 同时等,不是同时跑(单线程,但 I/O 等待不阻塞)
  • gather 省心wait 灵活

下一篇会深入事件循环内部,讲 epoll、回调注册、以及为什么 time.sleep() 会卡死整个事件循环。