直接调用阻塞代码会使事件循环停滞,导致协程串行执行、并发失效和cpu饥饿;应使用run_in_executor隔离或改用异步库。

如果您在 Python 协程中直接调用阻塞式代码(如 time.sleep()、requests.get() 或文件读写),事件循环将被挂起,导致所有其他协程无法调度执行。以下是分析该风险的具体方式:
一、阻塞协程导致事件循环停滞
asyncio 事件循环依赖于协程主动让出控制权(通过 await),一旦执行纯阻塞操作,线程将陷入等待,无法切换至其他待运行的协程,整个异步系统吞吐量急剧下降。
1、定义一个使用 time.sleep(2) 的异步函数:async def bad_coro(): time.sleep(2)。
2、在 asyncio.run() 中并发启动多个该协程:await asyncio.gather(bad_coro(), bad_coro(), bad_coro())。
立即学习“Python免费学习笔记(深入)”;
3、观察总耗时接近 6 秒而非约 2 秒,证实串行阻塞效应。
二、阻塞 I/O 操作阻断网络请求并发
同步 HTTP 请求库(如 requests)内部使用阻塞 socket 调用,在协程中调用会独占事件循环线程,使其他协程无法响应,丧失异步优势。
1、编写协程函数内调用 requests.get("https://httpbin.org/delay/1")。
2、使用 asyncio.create_task() 并发发起 5 次该请求。
3、实测总耗时约为 5 秒,而非预期的约 1 秒,表明请求被强制串行化。
三、CPU 密集型阻塞引发事件循环饥饿
长时间运行的 CPU 绑定操作(如大数组排序、加密计算)不触发 await,持续占用 CPU 时间片,使事件循环无机会轮询任务队列,造成协程“假死”。
1、在协程中执行 sum(i * i for i in range(10**7))。
2、同时启动另一协程尝试每 100ms 打印一次计数器。
3、观察到计数器输出严重滞后或中断,证明事件循环已被 CPU 计算完全阻塞。
四、使用 loop.run_in_executor 隔离阻塞调用
通过将阻塞代码提交至线程池或进程池执行,可避免事件循环主线程被占用,实现非阻塞封装。
1、获取当前事件循环:loop = asyncio.get_running_loop()。
2、调用 loop.run_in_executor(None, time.sleep, 2) 替代直接 sleep。
3、对 requests.get 使用相同方式封装后,并发 5 次请求总耗时恢复至约 1 秒级别。
五、采用原生异步替代库规避阻塞
替换同步依赖为专为 asyncio 设计的异步库,从源头消除阻塞调用,确保事件循环持续可调度。
1、将 requests 替换为 httpx.AsyncClient 或 aiohttp.ClientSession。
2、将 open() 文件操作替换为 aiofiles.open() 并配合 async/await 使用。
3、将 json.loads() 等 CPU 密集解析逻辑移入 loop.run_in_executor 处理。









