线程池大小不能简单设为CPU核数的2倍,需按任务类型区分:CPU密集型用min(核心数,并发请求数),I/O密集型需压测确定;注意内存占用、GIL切换、线程安全等问题。

线程池大小设成 CPU 核数的 2 倍就对了?
不对。硬套“2×CPU 核数”在 I/O 密集型场景下会严重拖慢吞吐,比如大量 httpx.get() 或数据库查询;而在纯计算任务里又可能引发频繁上下文切换。关键看任务类型:CPU 密集型用 min(核心数, 并发请求数),I/O 密集型得靠压测反推。
实操建议:
- 先用
psutil.cpu_count(logical=False)拿到物理核数,别直接用os.cpu_count() - I/O 类任务(如调第三方 API)初始线程数从
10起步,逐步加到50,观察ThreadPoolExecutor.submit()的平均延迟和失败率 - 避免把数据库连接池大小和线程池混为一谈——前者控制连接数,后者控制并发执行单元,两者不等价
用 concurrent.futures.ThreadPoolExecutor 还是插件?
绝大多数情况用标准库 ThreadPoolExecutor 就够了,它稳定、无额外依赖、API 清晰。所谓“线程池插件”(如 threadpoolctl 或某些 Web 框架封装)往往只是加了监控或自动伸缩逻辑,并不改变底层行为。
实操建议:
- 别为了“高级感”引入
threadpoolctl——它只管 OpenMP/BLAS 线程,对 Python 自己的ThreadPoolExecutor完全无效 - Django/Flask 项目里看到的 “线程池中间件”,本质是把
ThreadPoolExecutor实例挂到app.state或全局变量,自己写就行 - 需要动态调参?直接存一个
executor实例,用executor.shutdown(wait=False)+ 新建实例替换,别试图热改已运行的池
max_workers 设太小会超时,设太大内存爆掉?
会。每个线程默认占 8MB 栈空间(Linux 下可查 /proc/<pid>/limits</pid> 的 Stack size),1000 个线程就是 8GB 内存,还没算任务对象开销。同时,过多线程会让 GIL 切换更频繁,实际吞吐反而下降。
实操建议:
- 上线前用
tracemalloc抓住单次任务内存峰值,再乘以预期并发数,留 30% 余量定max_workers - 给
submit()加timeout参数,否则一个卡死的任务会占住整个线程,导致后续任务排队饿死 - 遇到
BrokenThreadPool或RuntimeError: can't start new thread,优先检查是否漏调shutdown(),而不是盲目扩大小
为什么用了线程池,_thread_handling 相关错误还在?
因为 _thread_handling 不是线程池的问题,而是你在非线程安全上下文里误用了共享状态。典型如:多个线程共用一个未加锁的 dict 计数器、或在 Flask 的 g 对象里存可变数据。
实操建议:
- 查错误栈里是否出现
KeyError、AttributeError或RuntimeError: dictionary changed size during iteration—— 这些都是典型的竞态信号 - 用
threading.local()替代全局变量,每个线程拿到独立副本,比手动加Lock更轻量 - Web 框架中慎用
app.config存运行时状态,它不是线程隔离的;改用flask.g或contextvars.ContextVar
线程池本身不制造竞态,但它会放大你代码里原本被低并发掩盖的线程安全漏洞。问题不在池子,在用池子的人有没有意识到“共享即风险”。










