当前位置: 首页 > news >正文

别被 `run_in_threadpool` 骗了,它只是个“背锅侠”!

如果你在写 FastAPI 或者基于 Starlette 的应用,那你一定遇到过这种进退两难的时刻:
你手里有一段祖传的同步阻塞代码(比如老旧的requests.get或者某个不支持异步的数据库驱动),但你的路由是被async def定义的“纯血异步”函数。

这时候,如果你直接把同步代码塞进去,整个异步事件循环就会像被施了定身法一样,瞬间卡死,吞吐量直接清零。

于是,你开始疯狂查阅文档,终于找到了那个金光闪闪的 API——run_in_threadpool
你兴奋地把它包在同步代码外面,一跑,果然不卡了!你长舒一口气,以为自己完美解决了并发问题。

但真相是:你并没有解决阻塞,你只是花钱雇了个“背锅侠”。而且,如果不了解它的底细,这个背锅侠迟早会把你的服务器搞垮。

今天,我们就来扒一扒run_in_threadpool的底裤,看看这玩意儿到底是个啥。


第一幕:微波炉与傻站着的厨师

要彻底懂这其中的奥秘,我们先来复习一下“同步(Sync)”和“异步(Async)”到底有什么区别。

把我们的程序想象成一家高档餐厅,而服务器的 CPU 和主事件循环,就是这家餐厅里唯一的超级服务员

真正的异步(比如httpx.AsyncClient):微波炉模式
服务员接到客人的点单,把菜放进微波炉,按下 10 分钟倒计时。然后立刻转身去招呼其他几百个客人。微波炉“叮”一声响了(I/O 完成通知),服务员再去端菜。
结果:单核(一个服务员)轻松应对千万并发,没有任何人闲着。

同步阻塞(比如requests.get):傻站着模式
服务员接到点单,把菜下锅,然后死死盯着这口锅看 10 分钟。这 10 分钟内,门外排队的几百个客人全都在骂街,因为服务员被卡住了。
结果:吞吐量暴跌,服务器“假死”。


第二幕:run_in_threadpool登场,背锅侠就位!

为了防止超级服务员被这口锅卡死,FastAPI 祭出了run_in_threadpool这块创可贴。

当你使用它时,到底发生了什么?这段代码奇迹般地变成微波炉了吗?
绝对没有!这道菜依然需要人站在锅边死等!

run_in_threadpool只是做了一个障眼法:
超级服务员一看这道菜要死等,为了不让自己被骂,他立刻跑到后厨,花钱雇了一个临时工(开辟了一个子线程)

服务员对临时工说:“老哥,你帮我站在这口锅前死等 10 分钟,好了叫我哈!”
随后,超级服务员一身轻松,立刻转身回大堂继续接客了。

这,就是run_in_threadpool的真相:它并没有把同步变异步,它只是把“阻塞卡顿”的这口锅,甩给了后台新建的子线程。

对于主程序(超级服务员)来说,他不卡了;但对于这段代码本身,它依然是阻塞的。


第三幕:定时炸弹——“临时工”是有限的!

既然能雇临时工,那我把所有同步代码都用run_in_threadpool包起来,不就天下太平了?

如果你敢这么干,你的服务器离崩溃就不远了。因为这种“假异步”有一个致命的弱点:线程池的数量是有上限的。

在 FastAPI(底层依赖 anyio)中,默认的线程池大小通常是40 个
这意味着,你的后厨最多只能容纳 40 个临时工。

  • 假设你同时来了 40 个很慢的请求,超级服务员雇了 40 个临时工,大家都在后厨死等。
  • 当第41 个请求来的时候,灾难发生了。
  • 服务员回头一看:“卧槽,雇不到人了!” 此时,这第 41 个请求只能在大堂苦苦排队。
  • 从用户的角度来看,你的服务器依然卡死了,不管你怎么刷新都在转圈。

而如果是真正的异步(微波炉模式),哪怕同时来 10000 个请求,服务员只需要把 10000 盘菜放进 10000 个微波炉按一下按钮就行了,单线程就能搞定,根本不需要雇佣临时工。


第四幕:终极禁忌——千万别让临时工干“体力活”!

如果你觉得线程池耗尽已经够惨了,那run_in_threadpool还有一个能让 Python 直接吐血的禁忌:用它来执行 CPU 密集型任务(纯粹的数值计算、图像处理等)。

这又回到了 Python 的祖传大坑——GIL(全局解释器锁)

在同一间厨房(进程)里,有且只有一把“炒菜用的铁铲(GIL)”。

  • 如果是 I/O 阻塞(比如等网络请求):临时工是在“傻等”,不需要拿铁铲(会释放 GIL),超级服务员可以继续拿铁铲干活,两不相干。
  • 如果是重度计算:临时工必须死死握住这把铁铲疯狂干活。这时候,超级服务员如果想去大堂端盘子(执行 Python 异步逻辑),就会发现铁铲被临时工抢走了!两人开始疯狂争夺同一把铁铲。

最终的结果是:你的代码不仅依然只能在一个 CPU 核心上跑,而且因为主线程和子线程疯狂抢夺 GIL,整体性能反而比不加还要慢!


总结法则:拿捏 FastAPI 并发的正确姿势

写到这里,是时候总结一波避坑口诀了:

  1. 原汤化原食:在异步框架里,永远首选原生支持 async 的第三方库(如httpx,asyncpg,aiofiles)。这是真理,是王道。
  2. 创可贴用法:如果迫不得已必须调用同步 I/O 库,并且确定并发量不高,可以用run_in_threadpool(或者直接写普通的def路由,FastAPI 底层也是放进线程池)。它能救急,但别当饭吃。
  3. 计算任务靠边站:如果你有消耗大量 CPU 的任务(视频处理、大矩阵运算),请绝对远离run_in_threadpool!正确做法是开辟全新的厨房,使用ProcessPoolExecutor或外部任务队列(如 Celery + Redis),让它们在其他多核 CPU 上奔跑。

别再被run_in_threadpool骗了,它只是个尽职尽责的背锅侠。善待它,别把它累死。

http://www.jsqmd.com/news/659332/

相关文章:

  • 清音刻墨Qwen3进阶技巧:参数调整与批量处理功能详解
  • DeepSeek-OCR效果展示:会议纪要扫描件→带标题/列表/引用的Markdown
  • GLM-4.1V-9B-Base嵌入式AI实践:在STM32生态中的轻量化部署探索
  • SAP硬件选择详解:服务器、存储与网络的全面解析
  • 笔试训练48天:删除公共字符
  • vLLM-v0.17.1效果展示:16K上下文下PagedAttention内存节省65%
  • AI训练硬件指南:GPU算力梯队与任务匹配框架
  • Stable Diffusion v1.5 实战案例:如何用提示词控制生成图片的风格与细节
  • 给嵌入式新手的CAN总线保姆级入门:从差分信号到数据帧,手把手带你理解汽车通信基石
  • MusePublic圣光艺苑完整指南:CSDN图床集成+真迹分享链接生成机制
  • STM32实战:旋转编码器防抖的3种方法对比(附F407完整代码)
  • SpringBoot实战:仿小红书源码中的内容发布链路拆分与事务控制
  • Phi-4-mini-reasoning 3.8B 智能文档处理:Typora风格Markdown内容自动生成
  • vue openlayers地图加载大量点位时优化
  • C语言这么牛,它自身又是用什么语言写的?真相很硬核
  • 手把手教你用AI手势识别:上传图片秒出彩虹骨骼图,无需编程
  • 别再自己画封装了!用这三个免费网站,5分钟搞定AD原理图和PCB库
  • Ostrakon-VL终端快速上手:扫码登录+微信小程序联动方案
  • GLM-OCR模型Java开发集成指南:SpringBoot微服务中的文档处理实战
  • Clawdbot代理网关快速上手:5分钟部署Qwen3:32B本地大模型
  • 用 Gemini 打造 10 分钟完美行程的五个“降维打击”技巧
  • 8、新的开始:返璞归真,使用最简单的ElementPlus来实现本项目
  • 【好靶场】你知道unionId吗
  • GEO 1.0 到 2.0:为什么 90% 的品牌优化是表面功夫
  • Jetson Orin Nano开发者必看:PyTorch环境一键配置指南(附常见错误排查)
  • AI超清画质增强自动化流水线:CI/CD集成思路
  • 华为eNSP静态路由与动态路由综合实验报告
  • Qwen3-14B私有部署成本分析:RTX 4090D云主机月度费用测算
  • 供应商评估模型:从课程设计、讲师背景、案例库到售后支持的全方位对比
  • 别再死记硬背APB时序了!用状态机手把手教你写一个可复用的APB Master模块(Verilog代码详解)