本文带大家用自顶向下的方式理解 Python 协程。看完你能回答这三个问题:
async def 和 await 到底是什么?
create_task 和直接 await 有什么区别?
- 怎么让三个任务同时跑、哪个先好就先处理哪个?
一、为什么需要协程?
想象你在厨房里做饭:
- 同步(一个人做饭):红烧肉焯水的时候你干等着,水开了才去切菜
- 多线程(请了三个厨师):三个厨师各做各的,但厨房太小容易撞在一起,还要协调谁用哪个灶
- 协程(一个厨师 + 眼观六路):红烧肉焯水的时候你去切菜,水开了回来处理——一个人把所有事安排得明明白白
协程的核心理念就是:在等待 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))
|
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) print(hello())
|
协程函数是定义,协程对象是调用定义得到的暂停的函数快照。事件循环只能调度协程对象。
三、并发:Task 与 create_task
3.1 直接 await 是串行的
1 2 3 4
| 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
| 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("我好困"))
|
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), )
|
等价于手动 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())
f.set_result("你好") print(f.done()) 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() 会卡死整个事件循环。