Python 的 async/await 是怎么工作的?为什么用了异步,接口还是会卡住?
🧑💻 面试官:AI 接口要调用几个外部服务,你准备用什么方式减少等待?
🙋♂️ 我:把函数改成 async,再用 await 调用,等待接口的时候就不会阻塞了。
🧑💻 面试官:函数里仍然使用同步的 HTTP 客户端,也能不阻塞吗?
🙋♂️ 我:那客户端也需要支持异步。
🧑💻 面试官:两个异步查询,你先 await 第一个,再 await 第二个,它们就会同时执行吗?
这道题的关键是:「让出执行机会」。async 不是把函数自动放到另一个线程;用了 await,也不代表两项任务已经同时开始。
面试速答(60 秒版)
Python 的 async/await 主要用于协作式并发。多个任务可以交给事件循环调度,一个任务遇到需要等待的异步操作时,可以暂停,让其他任务继续执行。
但是,把函数写成 async,并不会自动把里面的同步操作变成异步。如果仍然直接使用同步 HTTP 请求、time.sleep,或者长时间执行计算,就可能占住事件循环,让其他请求一起等待。
同时,先 await 一个查询,再开始另一个查询,通常还是顺序执行。对于已经确认互不依赖的异步查询,可以用 asyncio.gather 等方式一起调度。
因此,需要等待外部服务的任务,优先使用异步客户端;旧的阻塞调用可以考虑放到线程中执行。计算密集任务则需要另外安排进程或工作服务,不能指望加上 async 就自动变快。

知识点详解:await 的时候,程序究竟在做什么?
一个请求在等,为什么另一个请求还能执行?
假设一个接口需要查询订单服务和物流服务。两个服务都要过一会儿才返回,等待期间,本地程序没有多少事情需要计算。
如果当前请求一直占住执行线程,其他任务就没有机会运行。异步 I/O 则可以让程序先登记“我正在等这个结果”,把执行机会交给事件循环,等结果准备好了,再继续当前任务。
事件循环负责检查哪些任务已经可以继续,并安排它们执行。这里的多个任务并发推进, 不等于多个任务的 Python 代码正在同一线程里同时计算。
Python 的开发说明指出,同一个事件循环线程中,一个 Task 执行代码时,其他 Task 不会同时执行;当前任务在合适的等待点暂停,其他任务才有机会推进。
也不是见到 await 就一定切换任务。如果等待对象已经准备好了,当前任务可能直接继续。所以,不能用 await 的数量判断程序是不是“足够异步”。
async 只声明函数,不替你改掉同步操作
在 Python 中,调用 async 函数,会得到一个协程对象。需要进一步 await 它,或者把它调度成任务,它里面的过程才会执行。
但开始执行以后,函数体里仍然可能出现阻塞。例如:
time.sleep(2) 会让当前线程直接停下来,事件循环也没法使用这个线程处理其他任务。同步 HTTP 客户端等待网络响应时,也可能产生同样的问题。
如果把它换成 await asyncio.sleep(2) ,当前任务等待期间,事件循环就可以推进别的任务。但实际业务里的 HTTP 调用,还需要使用支持异步 I/O 的客户端,不能只在同步函数外面加一个 await。
已有的同步库暂时不能替换时,可以考虑 asyncio.to_thread ,让阻塞调用在另一个线程里执行,事件循环等待它的结果。Python 的 Task 文档给出了这种用法。
不过,线程池也有容量限制。调用量很大时,任务可能改为等待线程空闲,并没有凭空消除工作量。
两个 await,为什么还可能是串行的?
咱们再看订单和物流查询。假设参数都已准备好,两项查询互不依赖。
如果先写“等待订单查询完成”,再写“开始物流查询并等待”,那么第二个查询仍然要等第一个结束以后才开始。
下面两个版本都可以独立运行,用短暂等待模拟外部查询。不需要模型密钥,也不表示真实接口耗时。
Python
import asyncio
async def query(name):
# 模拟可以让出执行机会的 I/O 等待
await asyncio.sleep(0.05)
return name
async def main():
results = await asyncio.gather(
query("订单"), query("物流")
)
print(results)
asyncio.run(main())gather 会把两个协程一起调度,并等待结果。对于这个模拟,二者的等待可以重叠;如果改成先 await query("订单") ,再 await query("物流") ,就没有这样的重叠。
TypeScript
async function query(name: string): Promise {
// 模拟异步等待,不在等待期间占住执行线程
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
return name;
}
async function main() {
const results = await Promise.all([
query("订单"), query("物流"),
]);
console.log(results);
}
main().catch(console.error);TypeScript 这里沿用 JavaScript 的执行机制。调用 async 函数会开始执行函数体,并返回 Promise;这一点和 Python 调用后先得到协程对象不同。MDN 的 async 文档说明了这个行为。
两个例子要说明的是同一个问题:先把互不依赖的异步工作安排起来,再等待结果。Promise.all 或 gather 本身不会把同步计算自动变成并行计算。

接口还是卡,就继续检查谁没有让出执行机会
假设程序用了异步 HTTP 客户端,但返回结果以后,需要在接口里处理一份很大的文件。如果这段处理持续占用事件循环线程,其他请求仍然可能被拖慢。
这时候,需要区分等待外部 I/O 和本地计算。阻塞 I/O 可以考虑线程;耗时计算可以安排到进程池或独立工作服务,再由接口异步等待结果。
对于常见启用 GIL 的 CPython,不能把 to_thread 当成让普通 Python 计算自动多核并行的方案。某些释放 GIL 的扩展,以及不同运行时配置,需要另行判断。
此外,还要检查连接池、线程池、下游限流和任务队列。异步减少的是不必要的占用,不代表资源无限。大量任务一起等待同一个下游,也可能让排队、超时和重试增加。
面试官继续追问
使用异步以后,耗时就一定减半吗?
不会。两个互不依赖的等待可以重叠,但请求启动、结果处理和其他步骤仍然需要时间。
如果第二步必须使用第一步的结果,就不能为了并发提前执行。能省多少,需要看实际依赖和测量结果。
CPU 使用率不高,为什么接口还慢?
可能在等连接、等线程、等下游,也可能被某个同步操作占住事件循环。
需要记录任务从排队到执行的过程,检查事件循环是否长时间无法调度,而不是只看 CPU 使用率。
用 gather,一项失败后其他项就取消了吗?
默认情况下,gather 把异常传播给等待方,不会仅因一个任务出错,就取消其他任务。
Promise.all 也不会自动取消已经发出的请求。因此,异常后需要按调用规则处理其他任务,不能认为收到错误就等于所有工作停止了。
面试速记卡
- async:声明异步函数,不会自动改造函数体里的同步操作。
- await:等待可等待对象,必要时让出当前任务的执行机会。
- 事件循环:在同一线程中调度任务,不等于代码同时多核执行。
- 并发等待:先安排互不依赖的任务,再一起等待结果。
- 阻塞处理:使用异步客户端,或把合适的同步 I/O 放到线程。
- 计算任务:另外安排执行资源,不靠添加 async 自动提速。