创建时间: 2026-08-25最后更新: 2026-08-25

1. 函数

普通函数从第一行开始执行,直到返回结果或抛出异常。调用方会停在函数调用的位置,等函数结束后再继续:

normal-function.py
01
def greet(name):
02
print("函数开始")
03
result = f"你好,{name}"
04
print("函数结束")
05
return result
06
07
print("调用之前")
08
message = greet("小明")
09
print("调用之后")
10
print(message)

输出顺序是:

output.txt
1
调用之前
2
函数开始
3
函数结束
4
调用之后
5
你好,小明

greet("小明") 会立即进入函数体。调用方拿到的是函数执行完成后的字符串,而不是函数本身。

协程与普通函数最大的区别,是协程可以在执行过程中暂停,并在等待的结果准备好后恢复。这个能力让同一个线程可以在等待期间处理其他任务。

使用 async def 定义协程函数:

coroutine-function.py
1
async def greet(name):
2
print("协程开始")
3
return f"你好,{name}"
4
5
coroutine = greet("小明")
6
print(type(coroutine).__name__) # coroutine

这里需要区分两个概念:

名称示例含义
协程函数greet使用 async def 定义的函数
协程对象greet("小明")一次尚待执行的协程调用

调用普通函数会立即执行函数体,调用协程函数则只会创建协程对象。所以下面的代码不会输出「协程开始」:

create-only.py
1
async def greet():
2
print("协程开始")
3
4
print("创建之前")
5
coroutine = greet()
6
print("创建之后")
7
8
coroutine.close() # 示例没有运行协程,主动关闭以释放它

协程对象可以理解为「这段异步工作的一次执行计划」。创建它之后,还需要使用 await 等待,或者把它包装成 Task 交给事件循环调度,函数体才会真正开始执行。

如果只创建协程对象却没有等待、调度或关闭它,程序结束时通常会看到:

warning.txt
1
RuntimeWarning: coroutine 'greet' was never awaited

这个警告表示异步函数虽然被调用了,但对应的工作从未真正执行。

2. 生命周期

协程不是只能执行一次的普通代码块,它具有暂停和恢复的能力。一次协程调用通常经历以下过程:

  1. 调用协程函数,创建协程对象;
  2. 协程被 await 或包装成 Task,开始运行;
  3. 协程执行到尚未完成的等待操作;
  4. 当前协程暂停,把执行权交还给事件循环;
  5. 等待的 IO、定时器或 Future 完成;
  6. 事件循环恢复协程,从 await 后面继续;
  7. 协程返回结果、抛出异常或被取消。

下面通过日志观察暂停和恢复:

lifecycle.py
01
import asyncio
02
03
async def fetch():
04
print("2. fetch 开始")
05
await asyncio.sleep(1)
06
print("4. fetch 恢复")
07
return "请求结果"
08
09
async def main():
10
print("1. 准备等待 fetch")
11
result = await fetch()
12
print("5. main 恢复")
13
print(result)
14
15
asyncio.run(main())

fetch() 运行到 await asyncio.sleep(1) 时暂停。一秒后,事件循环让它继续执行。代码的实际输出是:

lifecycle-output.txt
1
1. 准备等待 fetch
2
2. fetch 开始
3
4. fetch 恢复
4
5. main 恢复
5
请求结果

在第二行和第四行之间,fetch() 处于暂停状态,事件循环可以处理其他任务。协程暂停时会保留当前执行位置和局部变量,恢复后不需要从函数第一行重新开始。

3. await

await 只能写在 async def 中,用于等待一个可等待对象(awaitable)。常见的可等待对象包括:

对象作用
协程对象表示一次尚待执行的异步调用
Task已经交给事件循环调度的协程
Future表示未来才会得到的结果

执行 await read_cache() 时,并不是一看到 await 就立即切换到另一个任务。控制权会先进入 read_cache():

await-chain.py
01
import asyncio
02
03
async def read_cache():
04
print("2. 进入 read_cache")
05
await asyncio.sleep(1)
06
print("3. read_cache 恢复")
07
return "缓存数据"
08
09
async def main():
10
print("1. main 开始")
11
result = await read_cache()
12
print("4. main 恢复")
13
print(result)
14
15
asyncio.run(main())

调用过程是:

  1. main() 执行到 await read_cache();
  2. read_cache() 开始运行;
  3. read_cache() 执行到尚未完成的 await asyncio.sleep(1);
  4. read_cache() 和等待它的 main() 都暂时不能继续;
  5. 控制权回到事件循环,事件循环可以调度其他任务;
  6. 定时器完成后,read_cache() 先恢复并返回结果;
  7. main() 得到结果,再从自己的 await 后面恢复。

因此,真正让控制权回到事件循环的,是协程调用链遇到一个尚未完成、需要等待的操作。如果被等待的协程可以立即计算并返回,整个调用链可能会连续执行,中间没有机会切换其他任务。

await 不会创建线程,也不会自动让代码在后台运行。它表达的是「当前协程依赖这个结果,可以暂停等待」。

4. 串行

异步代码不一定并发。连续直接等待两个协程时,第二个协程必须等第一个协程结束后才会创建并运行:

serial-await.py
01
import asyncio
02
import time
03
04
async def fetch(name):
05
print(f"{name} 开始")
06
await asyncio.sleep(1)
07
print(f"{name} 完成")
08
return name
09
10
async def main():
11
started_at = time.perf_counter()
12
13
first = await fetch("任务一")
14
second = await fetch("任务二")
15
16
elapsed = time.perf_counter() - started_at
17
print(first, second)
18
print(f"耗时约 {elapsed:.1f} 秒")
19
20
asyncio.run(main())

两个任务各等待 1 秒,总耗时接近 2 秒。这里使用了协程和 await,但执行关系仍然是串行的。

如果两个任务相互独立,可以把它们同时交给事件循环:

concurrent-await.py
01
import asyncio
02
import time
03
04
async def fetch(name):
05
print(f"{name} 开始")
06
await asyncio.sleep(1)
07
print(f"{name} 完成")
08
return name
09
10
async def main():
11
started_at = time.perf_counter()
12
13
first, second = await asyncio.gather(
14
fetch("任务一"),
15
fetch("任务二"),
16
)
17
18
elapsed = time.perf_counter() - started_at
19
print(first, second)
20
print(f"耗时约 {elapsed:.1f} 秒")
21
22
asyncio.run(main())

两个任务的等待时间可以重叠,总耗时接近 1 秒。这里先建立「直接 await 通常保持调用顺序,显式调度多个协程才能形成并发」的认识;Task、gather() 和任务取消会在后续文章中详细讲解。

5. 结果

协程使用 return 返回结果。等待它的调用方会得到这个值:

coroutine-result.py
01
import asyncio
02
03
async def add(left, right):
04
return left + right
05
06
async def main():
07
result = await add(2, 3)
08
print(result) # 5
09
10
asyncio.run(main())

如果协程抛出异常,异常会沿着 await 调用关系传递给等待者,可以使用普通的 try...except 处理:

coroutine-error.py
01
import asyncio
02
03
async def divide(left, right):
04
if right == 0:
05
raise ValueError("除数不能为 0")
06
07
return left / right
08
09
async def main():
10
try:
11
result = await divide(1, 0)
12
print(result)
13
except ValueError as error:
14
print(error)
15
16
asyncio.run(main())

这里不需要为了“让函数变异步”而添加无意义的 await asyncio.sleep(0)。async def 已经定义了协程函数;是否需要 await,取决于函数内部是否真的要等待异步操作。

一个协程对象表示一次执行,完成后不能再次等待:

reuse-error.py
01
import asyncio
02
03
async def get_message():
04
return "你好"
05
06
async def main():
07
coroutine = get_message()
08
print(await coroutine)
09
# print(await coroutine) # RuntimeError:不能重复使用已等待的协程
10
11
asyncio.run(main())

如果需要多次执行,应多次调用协程函数,分别创建新的协程对象。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容