1. 函数
普通函数从第一行开始执行,直到返回结果或抛出异常。调用方会停在函数调用的位置,等函数结束后再继续:
01def greet(name):02print("函数开始")03result = f"你好,{name}"04print("函数结束")05return result0607print("调用之前")08message = greet("小明")09print("调用之后")10print(message)
输出顺序是:
1调用之前2函数开始3函数结束4调用之后5你好,小明
greet("小明") 会立即进入函数体。调用方拿到的是函数执行完成后的字符串,而不是函数本身。
协程与普通函数最大的区别,是协程可以在执行过程中暂停,并在等待的结果准备好后恢复。这个能力让同一个线程可以在等待期间处理其他任务。
使用 async def 定义协程函数:
1async def greet(name):2print("协程开始")3return f"你好,{name}"45coroutine = greet("小明")6print(type(coroutine).__name__) # coroutine
这里需要区分两个概念:
| 名称 | 示例 | 含义 |
|---|---|---|
| 协程函数 | greet | 使用 async def 定义的函数 |
| 协程对象 | greet("小明") | 一次尚待执行的协程调用 |
调用普通函数会立即执行函数体,调用协程函数则只会创建协程对象。所以下面的代码不会输出「协程开始」:
1async def greet():2print("协程开始")34print("创建之前")5coroutine = greet()6print("创建之后")78coroutine.close() # 示例没有运行协程,主动关闭以释放它
协程对象可以理解为「这段异步工作的一次执行计划」。创建它之后,还需要使用 await 等待,或者把它包装成 Task 交给事件循环调度,函数体才会真正开始执行。
如果只创建协程对象却没有等待、调度或关闭它,程序结束时通常会看到:
1RuntimeWarning: coroutine 'greet' was never awaited
这个警告表示异步函数虽然被调用了,但对应的工作从未真正执行。
2. 生命周期
协程不是只能执行一次的普通代码块,它具有暂停和恢复的能力。一次协程调用通常经历以下过程:
- 调用协程函数,创建协程对象;
- 协程被
await或包装成 Task,开始运行; - 协程执行到尚未完成的等待操作;
- 当前协程暂停,把执行权交还给事件循环;
- 等待的 IO、定时器或 Future 完成;
- 事件循环恢复协程,从
await后面继续; - 协程返回结果、抛出异常或被取消。
下面通过日志观察暂停和恢复:
01import asyncio0203async def fetch():04print("2. fetch 开始")05await asyncio.sleep(1)06print("4. fetch 恢复")07return "请求结果"0809async def main():10print("1. 准备等待 fetch")11result = await fetch()12print("5. main 恢复")13print(result)1415asyncio.run(main())
fetch() 运行到 await asyncio.sleep(1) 时暂停。一秒后,事件循环让它继续执行。代码的实际输出是:
11. 准备等待 fetch22. fetch 开始34. fetch 恢复45. main 恢复5请求结果
在第二行和第四行之间,fetch() 处于暂停状态,事件循环可以处理其他任务。协程暂停时会保留当前执行位置和局部变量,恢复后不需要从函数第一行重新开始。
3. await
await 只能写在 async def 中,用于等待一个可等待对象(awaitable)。常见的可等待对象包括:
| 对象 | 作用 |
|---|---|
| 协程对象 | 表示一次尚待执行的异步调用 |
| Task | 已经交给事件循环调度的协程 |
| Future | 表示未来才会得到的结果 |
执行 await read_cache() 时,并不是一看到 await 就立即切换到另一个任务。控制权会先进入 read_cache():
01import asyncio0203async def read_cache():04print("2. 进入 read_cache")05await asyncio.sleep(1)06print("3. read_cache 恢复")07return "缓存数据"0809async def main():10print("1. main 开始")11result = await read_cache()12print("4. main 恢复")13print(result)1415asyncio.run(main())
调用过程是:
main()执行到await read_cache();read_cache()开始运行;read_cache()执行到尚未完成的await asyncio.sleep(1);read_cache()和等待它的main()都暂时不能继续;- 控制权回到事件循环,事件循环可以调度其他任务;
- 定时器完成后,
read_cache()先恢复并返回结果; main()得到结果,再从自己的await后面恢复。
因此,真正让控制权回到事件循环的,是协程调用链遇到一个尚未完成、需要等待的操作。如果被等待的协程可以立即计算并返回,整个调用链可能会连续执行,中间没有机会切换其他任务。
await 不会创建线程,也不会自动让代码在后台运行。它表达的是「当前协程依赖这个结果,可以暂停等待」。
4. 串行
异步代码不一定并发。连续直接等待两个协程时,第二个协程必须等第一个协程结束后才会创建并运行:
01import asyncio02import time0304async def fetch(name):05print(f"{name} 开始")06await asyncio.sleep(1)07print(f"{name} 完成")08return name0910async def main():11started_at = time.perf_counter()1213first = await fetch("任务一")14second = await fetch("任务二")1516elapsed = time.perf_counter() - started_at17print(first, second)18print(f"耗时约 {elapsed:.1f} 秒")1920asyncio.run(main())
两个任务各等待 1 秒,总耗时接近 2 秒。这里使用了协程和 await,但执行关系仍然是串行的。
如果两个任务相互独立,可以把它们同时交给事件循环:
01import asyncio02import time0304async def fetch(name):05print(f"{name} 开始")06await asyncio.sleep(1)07print(f"{name} 完成")08return name0910async def main():11started_at = time.perf_counter()1213first, second = await asyncio.gather(14fetch("任务一"),15fetch("任务二"),16)1718elapsed = time.perf_counter() - started_at19print(first, second)20print(f"耗时约 {elapsed:.1f} 秒")2122asyncio.run(main())
两个任务的等待时间可以重叠,总耗时接近 1 秒。这里先建立「直接 await 通常保持调用顺序,显式调度多个协程才能形成并发」的认识;Task、gather() 和任务取消会在后续文章中详细讲解。
5. 结果
协程使用 return 返回结果。等待它的调用方会得到这个值:
01import asyncio0203async def add(left, right):04return left + right0506async def main():07result = await add(2, 3)08print(result) # 50910asyncio.run(main())
如果协程抛出异常,异常会沿着 await 调用关系传递给等待者,可以使用普通的 try...except 处理:
01import asyncio0203async def divide(left, right):04if right == 0:05raise ValueError("除数不能为 0")0607return left / right0809async def main():10try:11result = await divide(1, 0)12print(result)13except ValueError as error:14print(error)1516asyncio.run(main())
这里不需要为了“让函数变异步”而添加无意义的 await asyncio.sleep(0)。async def 已经定义了协程函数;是否需要 await,取决于函数内部是否真的要等待异步操作。
一个协程对象表示一次执行,完成后不能再次等待:
01import asyncio0203async def get_message():04return "你好"0506async def main():07coroutine = get_message()08print(await coroutine)09# print(await coroutine) # RuntimeError:不能重复使用已等待的协程1011asyncio.run(main())
如果需要多次执行,应多次调用协程函数,分别创建新的协程对象。