← 返回题目列表

为什么不能在 async 函数里直接调用阻塞代码?会造成什么问题?

高频 中等 第 2 / 27 题 更新于 2026/07/27
阻塞调用事件循环asyncio

简化版

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.sleeprequests、同步数据库会卡住事件循环。能换异步库就换异步库,不能换就用 to_thread 或 executor 隔离。