Skip to content

04 · async/await 异步编程:一个人同时照看很多锅 ​

学完你能说出:

  • 协程、事件循环、await 分别是什么。
  • 为什么异步适合「同时发很多网络请求」,比如批量调用大模型 API。
  • 最常见的坑:在异步函数里调用了同步的阻塞函数,整个程序都卡住。

先看地图 ​

Python 模块最后一篇。第 01 篇说 IO 密集任务可以用多线程;这一篇讲另一种更轻量的方案:异步(asyncio)。FastAPI、各家大模型 SDK 都大量使用异步。

本篇讲 4 个点:① 同步 vs 异步 → ② 协程和事件循环 → ③ 并发执行很多请求 → ④ 什么时候不该用。

一句话本质 ​

异步 = 一个线程里,遇到「等待」就先去干别的,等好了再回来接着干。

生活类比:一个厨师煮 3 锅汤 ​

  • 同步:煮第一锅,站在锅边等 10 分钟,煮好再煮第二锅……3 锅要 30 分钟。
  • 多线程:请 3 个厨师,每人看一锅。10 分钟,但人工贵。
  • 异步:一个厨师把 3 锅都放上火,哪锅开了就去处理哪锅。约 10 分钟,只要一个人。
  • 关键前提:煮汤的大部分时间在等。如果是切菜(一直在干活,CPU 密集),一个人还是只能一刀一刀切,异步帮不上忙。

一步步看:调用 3 次大模型 API(每次等 2 秒,数字是示意) ​

同步写法:6 秒 ​

python
import time

def ask(q):
    time.sleep(2)              # sleep = 睡眠:模拟等待网络响应 2 秒
    return f"回答:{q}"

for q in ["问1", "问2", "问3"]:
    print(ask(q))              # 一个一个来:2 + 2 + 2 = 6 秒

异步写法:约 2 秒 ​

python
import asyncio                          # asyncio = async 异步 + io 输入输出

async def ask(q):                       # async def 定义一个「协程函数」
    await asyncio.sleep(2)              # await = 等待:在这里让出控制权,去干别的
    return f"回答:{q}"

async def main():
    results = await asyncio.gather(     # gather = 收集:同时启动多个协程,等全部完成
        ask("问1"), ask("问2"), ask("问3")
    )
    print(results)

asyncio.run(main())                     # run = 运行:启动事件循环,执行 main

时间线:

0 秒:启动「问1」→ 遇到 await,让出 → 启动「问2」→ 让出 → 启动「问3」→ 让出
       (3 个请求都在等网络)
2 秒:3 个都返回了 → 依次继续执行 → 全部完成

三个关键概念 ​

概念是什么类比
协程(Coroutine)用 async def 定义的函数,能在 await 处暂停、之后再继续一锅汤
事件循环(Event Loop)调度者:不停检查「哪个协程等好了」,让它继续跑厨师
await「这里要等,我先让出去」的标记把锅放上火,转身去看别的锅

为什么这么设计 ​

为什么有了多线程还要异步? ​

  • 问题:同时处理几千个网络请求时,开几千个线程开销很大(每个线程都占内存,切换也有成本),还要处理线程安全问题。
  • 方案:在一个线程里用协程,只在 await 处切换,切换由程序自己控制。
  • 好处:一个协程只占很少内存,轻松上万个;只在 await 处切换,不会在一行代码中间被打断,共享数据更不容易出错。

一句话答法

异步在单线程内用协程处理并发,开销远小于线程,适合大量 IO 等待;而且只在 await 处切换,比多线程更少出现竞争问题。

为什么要显式写 await? ​

  • 问题:事件循环需要知道「什么时候可以切换去干别的」。
  • 方案:程序员在会等待的地方明确写 await,表示「这里可以让出」。
  • 好处:切换点清清楚楚,读代码就知道哪里可能被打断。代价是:漏写 await、或者调用了不支持 await 的阻塞函数,整个程序都会卡住。

一句话答法

await 明确标出「这里在等,可以让出控制权」,事件循环据此切换协程;切换点可见,但也要求等待操作必须是可 await 的异步版本。

最大的坑:在异步函数里调用阻塞函数 ​

python
async def ask(q):
    time.sleep(2)          # ✗ 同步阻塞:厨师站在这锅前面发呆 2 秒,别的锅全没人管
    requests.get(url)      # ✗ requests 是同步库,同样会卡住整个事件循环

async def ask(q):
    await asyncio.sleep(2)              # ✓ 异步版本
    async with httpx.AsyncClient() as c:   # ✓ httpx 有异步客户端
        await c.get(url)

规则:在 async def 里,所有等待都要用异步版本(await 得了的)。实在要调用同步库,用 await asyncio.to_thread(函数, 参数) 把它放到别的线程里执行。

怎么选 ​

场景选择
大量网络请求(批量调 API、爬虫、Web 服务)异步
少量 IO,或者用的库只有同步版本多线程
CPU 密集计算多进程(见第 01 篇)

英文术语拆解 ​

术语拆开看一句话意思
asyncasynchronous 异步的声明一个异步(协程)函数
await等待在这里等结果,同时让出控制权
Coroutineco 共同 + routine 例程(函数)协程:能暂停和继续的函数
Event Loopevent 事件 + loop 循环事件循环:调度协程的核心
asyncioasync + io 输入输出Python 的异步标准库
gather收集并发运行多个协程,等它们全部完成
Blockingblock 阻塞 + -ing阻塞:调用后一直卡着,直到完成才返回
Concurrency / Parallelism并发 / 并行并发是交替推进,并行是真正同时执行

面试怎么问 ​

Q:协程和线程的区别?

线程由操作系统调度,可能在任意位置被切换,需要加锁保护共享数据,每个线程开销较大;协程在用户态由事件循环调度,只在 await 处主动让出,开销很小,适合大量 IO 并发。协程本身在单线程内运行,不能利用多核做计算。

Q:并发和并行的区别?

并发是多个任务交替推进,宏观上看起来同时在做;并行是多个任务在多个 CPU 核上真正同时执行。asyncio 实现的是并发,多进程可以实现并行。

Q:在 async 函数里调用 time.sleep 或 requests 会怎样?

会阻塞整个事件循环,所有协程都停住,异步失去意义。应改用异步版本(asyncio.sleep、httpx、aiohttp),或用 asyncio.to_thread 把同步调用放到线程里执行。

容易混的地方 ​

  • 调用协程函数不会执行它:ask("问1") 只是创建了一个协程对象,要 await 它、或交给 gather / run,才会真正执行。
  • async 不会让代码自动变快:只有存在大量等待、并且等待可以重叠时才有用。
  • 并发 ≠ 并行:一个厨师照看三锅是并发;三个厨师各看一锅是并行。

和其他知识的连接 ​

自测 3 问 ​

Q1:用「一个厨师煮 3 锅汤」类比,解释同步、多线程、异步的区别。

点开看答案

同步:一锅一锅煮,站着等每一锅(30 分钟)。多线程:请 3 个厨师各看一锅(快但人工贵)。异步:一个厨师把 3 锅都放上火,哪锅好了处理哪锅(快,而且只要一个人)。前提是大部分时间在等。

Q2:3 个各需等待 2 秒的请求,用 asyncio.gather 并发执行,大约需要多久?为什么?

点开看答案

大约 2 秒。每个协程遇到 await 就让出控制权,3 个请求的等待时间互相重叠,而不是 2 + 2 + 2。

Q3:在 async 函数里调用 time.sleep(2) 会发生什么?怎么改?

点开看答案

time.sleep 是同步阻塞调用,会让整个事件循环停 2 秒,所有协程都卡住。改成 await asyncio.sleep(2);如果必须调用同步函数,用 await asyncio.to_thread(...) 放到线程里执行。

✅ 验收清单 ​

  • 能用「厨师煮汤」类比,区分同步、多线程、异步
  • 能说出协程、事件循环、await 各自是什么
  • 能手写用 asyncio.gather 并发执行多个协程的代码,并说出耗时
  • 能讲清「有了多线程为什么还要异步」(问题 → 方案 → 好处)
  • 能说出在 async 函数里调用阻塞函数的后果和改法

下一步该懂什么 ​

  • 回到 AI 知识库:批量调用大模型、Agent 并发执行工具,都会用到这一篇。→ 工具调用
  • 想更深入:了解 FastAPI 里 async def 接口和普通 def 接口的区别。