0%

理解 Python 中的协程、Future、任务

理解这三者的关系是掌握 asyncio 异步编程的关键。我们可以用一个简单的比喻来贯穿整个解释:做饭


1. 协程 (Coroutine)

是什么?
协程是 asyncio 异步编程的基石。一个用 async def 关键字定义的函数,其返回值就是一个协程对象。协程是一个可以暂停和恢复执行的特殊函数。

核心特点:

  • 惰性执行:仅仅调用一个协程函数并不会执行其中的代码,它只会返回一个协程对象。
  • 需要驱动:协程必须被驱动才能运行。驱动方式通常有两种:
    1. 在另一个已经运行的协程中通过 await 关键字来调用。
    2. 通过 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
18
import 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)

是什么?
TaskFuture 的一个子类。它的特定作用是包装并独立调度一个协程在事件循环中并发执行Task 是实现并发的核心工具。

核心特点:

  • 并发执行:当你用 asyncio.create_task() 把一个协程包装成一个 Task 时,该协程会立即被提交到事件循环中,并尽快开始执行,而不需要你立即 await 它。这使得多个任务可以“同时”运行。
  • 可等待:因为 TaskFuture 的子类,所以它也是一个可等待对象。你可以随时 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
40
import 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)它完成并获取结果。