理解这三者的关系是掌握 asyncio
异步编程的关键。我们可以用一个简单的比喻来贯穿整个解释:做饭。
1. 协程 (Coroutine)
是什么?
协程是 asyncio
异步编程的基石。一个用 async def
关键字定义的函数,其返回值就是一个协程对象。协程是一个可以暂停和恢复执行的特殊函数。
核心特点:
- 惰性执行:仅仅调用一个协程函数并不会执行其中的代码,它只会返回一个协程对象。
- 需要驱动:协程必须被驱动才能运行。驱动方式通常有两种:
- 在另一个已经运行的协程中通过
await
关键字来调用。 - 通过
asyncio.run()
启动顶层协程,或通过asyncio.create_task()
将其包装成任务。
- 在另一个已经运行的协程中通过
await
关键字:await
只能在async def
函数内部使用。它会暂停当前协程的执行,让事件循环去处理其他任务,直到await
后面的可等待对象(另一个协程、任务或 Future)完成。
做饭的比喻:
协程就像一个菜谱 (Recipe)。菜谱详细描述了做一道菜的所有步骤(代码逻辑),包括需要在哪里“等待”(比如 await asyncio.sleep(10)
就好比菜谱里写着“将食材腌制10分钟”)。光有菜谱本身,菜是不会自己做出来的。
代码示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import asyncio
async def make_salad():
print("开始准备沙拉...")
# 这是一个耗时操作,比如洗菜、切菜
await asyncio.sleep(2) # 模拟IO操作,暂停执行,交出控制权
print("沙拉准备好了!")
return "一份美味的沙拉"
# 1. 调用协程函数,得到一个协程对象
salad_coro = make_salad()
print(f"调用 make_salad() 得到的是: {salad_coro}")
print("仅仅调用并不会执行任何代码")
# 2. 使用 asyncio.run() 来驱动协程,并实际执行它
# asyncio.run() 会创建事件循环,运行协程,然后关闭循环
result = asyncio.run(salad_coro)
print(f"最终结果: {result}")
2. Future 对象
是什么?Future
是一个低层级的可等待对象,它代表一个异步操作最终的结果。可以把它想象成一个“占位符”或“期货”,在未来的某个时刻,这个占位符会被一个真实的值或一个异常所填充。
核心特点:
- 状态:一个
Future
对象有自己的状态(比如pending
,finished
,cancelled
)。 - 不关心如何产生结果:
Future
对象本身不包含任何业务逻辑,它只关心最终的结果。 - 由底层库使用:通常,应用程序开发者不直接创建
Future
对象。它们更多地被底层的库(如网络库)用来与事件循环集成。例如,一个库在等待网络数据时,可以创建一个Future
,当数据到达时,就调用future.set_result()
来填充结果。
做饭的比喻:Future
就像你在快餐店点餐后拿到的那个取餐凭证/电子蜂鸣器。你不知道后厨具体是怎么做的,但你拿着这个凭证,就代表你未来会得到你的餐点。当蜂鸣器响起时(Future
完成了),你就可以凭它去取餐(获取结果)。
3. 任务 (Task)
是什么?Task
是 Future
的一个子类。它的特定作用是包装并独立调度一个协程在事件循环中并发执行。Task
是实现并发的核心工具。
核心特点:
- 并发执行:当你用
asyncio.create_task()
把一个协程包装成一个Task
时,该协程会立即被提交到事件循环中,并尽快开始执行,而不需要你立即await
它。这使得多个任务可以“同时”运行。 - 可等待:因为
Task
是Future
的子类,所以它也是一个可等待对象。你可以随时await
一个Task
来获取它的最终结果(如果它还没完成,await
会等待它完成)。 - 管理和控制:
Task
对象提供了取消任务 (task.cancel()
)、检查状态 (task.done()
) 等管理接口。
做饭的比喻:Task
就像是你把菜谱(协程)交给了一位厨师(事件循环),并让他开始工作。厨师会立即开始按照菜谱做菜,而你可以去做别的事情(比如摆放餐具)。你手里拿到了取餐凭证(Task 对象本身),可以随时用它来查看菜做得怎么样了,或者在最后等菜上桌 (await task
)。
代码示例: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
40import asyncio
import time
async def brew_coffee():
print("开始煮咖啡...")
await asyncio.sleep(3)
print("咖啡煮好了!")
return "一杯香浓的咖啡"
async def toast_bread():
print("开始烤面包...")
await asyncio.sleep(2)
print("面包烤好了!")
return "两片烤面包"
async def main():
start_time = time.time()
# 将协程包装成任务,它们会立即开始并发执行
coffee_task = asyncio.create_task(brew_coffee())
bread_task = asyncio.create_task(toast_bread())
# 在等待任务完成的同时,我们可以做点别的事
print("正在摆放餐具...")
await asyncio.sleep(1)
print("餐具摆放完毕。")
# 现在等待任务完成并获取结果
# 注意:这里我们分别 await,更好的方式是使用 asyncio.gather
coffee_result = await coffee_task
bread_result = await bread_task
# 使用 gather 会更简洁
# results = await asyncio.gather(coffee_task, bread_task)
end_time = time.time()
print(f"\n早餐准备完毕: {coffee_result} 和 {bread_result}")
print(f"总耗时: {end_time - start_time:.2f} 秒") # 结果约为3秒,而不是 3+2=5秒
asyncio.run(main())
在这个例子中,煮咖啡(3秒)和烤面包(2秒)是并发进行的,所以总耗时取决于最长的那个任务,也就是3秒左右,而不是串行执行的5秒。
总结:三者关系
概念 | 核心作用 | 比喻 | 如何创建/使用 |
---|---|---|---|
协程 (Coroutine) | 定义一个可以暂停的异步操作流程。 | 菜谱 | 通过 async def 定义函数。 |
Future | 代表一个异步操作的最终结果的占位符。 | 取餐凭证 | 通常由底层库创建和管理。 |
任务 (Task) | 调度和执行一个协程,实现并发。 | 将菜谱交给厨师去执行 | 通过 asyncio.create_task() 包装一个协程。 |
核心关系链:
你编写一个 协程 (Coroutine)(菜谱),然后通过 asyncio.create_task()
将它包装成一个 任务 (Task)(把菜谱交给厨师),这个 任务 本身就是一个特殊的 Future(你拿到的取餐凭证),你可以随时等待(await
)它完成并获取结果。