为什么不能在 async 函数里直接调用阻塞代码?会造成什么问题?
简化版
async 函数运行在事件循环中,如果里面直接调用 time.sleep()、requests.get()、同步数据库驱动等阻塞代码,会卡住整个事件循环,导致其他协程无法运行。异步代码要使用非阻塞库,或把阻塞函数放到线程池、asyncio.to_thread() 中执行。
详细版
错误示例:
import time
async def bad():
time.sleep(3)
这里会阻塞事件循环 3 秒。
正确替代:
import asyncio
async def good():
await asyncio.sleep(3)
同步 HTTP 的问题:
async def bad_fetch():
return requests.get("https://example.com").text
更合适的方式:
- 使用异步 HTTP 客户端,例如 aiohttp、httpx async client;
- 或临时用
asyncio.to_thread()包住同步阻塞函数; - CPU 密集任务考虑进程池。
面试重点:异步并发依赖事件循环能持续调度,一旦某个协程长期不让出控制权,整个异步系统都会变慢。
完整版教学
一、事件循环怕的不是慢,而是不让出控制权
协程并发的前提是:任务遇到等待时通过 await 把控制权还给事件循环。
如果你写:
time.sleep(5)
当前线程会真的睡 5 秒。事件循环也在这个线程里,所以它不能调度其他任务。
如果你写:
await asyncio.sleep(5)
当前协程暂停,事件循环可以去运行其他任务。
二、常见阻塞源
异步函数里最常见的阻塞调用包括:
time.sleep();requests.get();- 同步数据库客户端;
- 同步 Redis 客户端;
- 大文件同步读写;
- CPU 密集循环;
- 某些第三方 SDK 的同步 API。
这些调用在普通同步程序里没问题,但放进事件循环就会拖住所有协程。
三、异步库要整条链路配套
写 FastAPI 或 aiohttp 时,handler 是 async def,但如果里面调用同步库:
async def handler():
data = requests.get(url).json()
return data
这个接口仍然可能卡住事件循环。异步性能不是函数签名带 async 就有,而是整个 I/O 链路都要尽量异步。
例如:
async with httpx.AsyncClient() as client:
response = await client.get(url)
数据库也类似,要使用支持异步的驱动或框架接口。
四、必须调用同步函数怎么办
如果是短期兼容,可以用:
result = await asyncio.to_thread(blocking_func, arg1, arg2)
它会把阻塞函数放到线程中执行,避免卡住事件循环。
更早或更底层的写法是:
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, blocking_func)
如果是 CPU 密集计算,线程未必能真正并行,可能要考虑进程池。
五、如何发现事件循环被阻塞
现象包括:
- 所有请求一起变慢;
- 心跳任务延迟;
- timeout 不准;
- 日志显示某个协程长时间没有 await;
- CPU 飙高或某个同步调用耗时很长。
可以通过日志、profiling、asyncio debug mode、链路追踪等方式定位。工程里还可以给关键异步任务加耗时监控。
六、常见误区与追问
| 阻塞来源 | 问题表现 | 处理方式 |
|---|---|---|
time.sleep | 整个事件循环暂停 | 改用 asyncio.sleep |
| 同步 HTTP/DB 客户端 | 请求期间其他协程难推进 | 换异步客户端或丢线程 |
| CPU 密集计算 | 长时间占住循环线程 | 进程池或拆分计算 |
| 大文件同步 I/O | 读写期间阻塞循环 | 异步库、线程池或分块处理 |
- 误区:async 函数里写什么都会自动异步。
async def只提供协程语义,里面调用同步阻塞函数仍会阻塞事件循环。 - 误区:偶尔阻塞几十毫秒没影响。 高并发服务里一次阻塞会影响同一事件循环上的所有请求,尾延迟会被放大。
- 误区:把同步库包一层 await 就变异步了。 只有库本身在等待期间让出控制权,或者被放到线程/进程执行,才能避免卡住事件循环。
- 追问:如何快速发现事件循环被阻塞? 观察慢请求、开启 asyncio debug、记录任务调度延迟,或用监控看 event loop lag。
- 追问:同步函数必须调用怎么办? I/O 阻塞函数可用
asyncio.to_thread或 executor 隔离;CPU 密集任务考虑进程池。 - 追问:为什么异步链路要配套? 一层同步阻塞客户端就可能让整个异步调用链失去并发收益。
易错点:异步程序最怕的不是“函数慢”,而是“慢的时候不把事件循环还回去”。
七、加强记忆
异步函数里最怕“假异步”:外面写着 async def,里面全是阻塞调用。await 才是让出控制权的关键;time.sleep、requests、同步数据库会卡住事件循环。能换异步库就换异步库,不能换就用 to_thread 或 executor 隔离。