ThreadPoolExecutor(max_workers) vs threading.Semaphore

📅 2026/8/24 3:15:42
ThreadPoolExecutor(max_workers) vs threading.Semaphore
为什么有了ThreadPoolExecutor(max_workers)设置了最大数量还需要threading.Semaphore两者什么时候选哪个两者都能做「最多N个同时执行」但是能力范围不一样不是完全等价。1、ThreadPoolExecutor(max_workersN)withThreadPoolExecutor(max_workers2)aspool:foriinrange(10):pool.submit(worker,i)它控制的是这个线程池内部最多同时有 N 个 worker OS 线程在运行任务。特点管控范围只对提交到这个线程池里面的任务生效。外面自己手动开的Thread()完全不受它约束。队列在线程池内部任务提交多了在池的内部队列排队。职责线程复用、生命周期管理、异常捕获一站式。 局限只能管控交给自己的任务。别的地方新开线程它管不到。举例子你代码里有两处并发逻辑A模块用线程池B模块直接Thread(…).start()。max_workers只能限制A模块B模块疯狂开线程它拦不住。2、threading.Semaphore(N)semSemaphore(2)defworker(i):sem.acquire()# 业务sem.release()# 不管你怎么开线程都受限制foriinrange(10):Thread(targetworker,args(i,)).start()信号量是一个独立的计数器对象。不管这个任务是来自ThreadPoolExecutor还是手动Thread()只要代码里执行sem.acquire()就会受限流。管控的不是线程而是业务逻辑的临界区代码。 重点区别max_workers限制最多多少个worker线程。threading.Semaphore限制最多多少个任务可以进入某一段代码。什么时候两者效果一样所有任务全部丢进同一个线程池每个任务一进来就立刻访问受信号量保护的代码。此时max_workers3和Semaphore(3)看起来效果几乎一样。什么时候必须用 Semaphore不能用 max_workers场景①多处来源的任务要全局统一限流比如一处是 ThreadPoolExecutor 的任务另一处是别的地方手动创建的 Thread两处都要调用同一个外部API希望全局最多同时2次网络请求。线程池A的max_workers只管自己池子里的任务管不到另一组手动开的线程。这时就搞一个全局sem threading.Semaphore(2)所有要调用API的地方先acquire不管线程从哪来统一限流。类比iOS多处地方往不同GCD队列丢任务但是希望全局最多3个网络并发就用一个全局DispatchSemaphore各个队列的任务进来先wait。GCD队列本身只能管控自己队列做不到跨队列全局限流。场景②同一个线程内部多次进入临界区重入threading.Semaphore支持重入还有专门threading.BoundedSemaphore。注意不是RLockSemaphore可以一次拿多个许可acquire(blockingTrue,timeoutNone)。比如一个任务进来可以占用2个许可代表这个任务“消耗两份并发额度”。ThreadPool做不到线程就代表1份没有“一份任务占多份额度”的概念。举个例子有的大文件任务权重高一个任务相当于2个普通请求希望占用2个信号量许可。这只能Semaphore实现。场景③部分逻辑要限流部分逻辑不限流一个线程内部跑一些轻量计算不需要限流调用外部MinerU API这里才要限流defworker():# 这里随便跑不限流cpu_work()sem.acquire()call_mineru_api()# 仅仅这一段受并发控制sem.release()如果你用max_workers只要任务提交进线程池整个任务的生命周期就占用一条线程。哪怕大部分时间在跑无关代码线程资源也被占住。信号量可以做到只有真正调用API那一小段逻辑做闸门前面后面的代码不受限。那什么时候直接用 ThreadPoolExecutor(max_workers)不需要再加Semaphore绝大多数普通业务场景所有并发任务全部交给这一个线程池整个任务生命周期都差不多是IO不需要精细控制局部代码。就直接用max_workersN足够代码更简洁不需要额外信号量。容易踩坑两者叠加使用# 线程池最多5条线程信号量只允许2个进入API调用withThreadPoolExecutor(max_workers5)aspool:pool.submit(task)会出现5条OS线程都启动了其中3条线程卡在sem.acquire()原地阻塞休眠。5个操作系统线程被创建出来其中3个就干等休眠白白占用OS线程资源。 不推荐这么写。正确做法如果用线程池做IO限流尽量把max_workers直接设置成并发上限不要依靠信号量去二次闸门。总结一张表方案管控对象管控范围适合场景ThreadPoolExecutor(max_workers)worker操作系统线程仅本线程池提交的任务全部任务交给本池简单IO并发代码简洁threading.Semaphore临界区代码段所有调用acquire的线程不管来自哪里跨多处并发源全局限流部分代码段限流任务消耗多份额度映射回iOS GCDThreadPoolExecutor ≈ 一个GCD自定义并发队列设置DispatchQueue.init(attributes: .concurrent, maxConcurrentOperationCount 3)只管控这个队列内部。threading.Semaphore ≈DispatchSemaphore全局对象所有队列的任务都可以来wait跨队列做并发控制。回到MinerU那个业务如果用同步requests ThreadPoolExecutor直接设置max_workers3就够一般不需要再加threading.Semaphore。如果你的项目里面多处不同模块不同线程来源都调用MinerU API想要全局控制总并发那就搞一个全局threading.Semaphore(3)。而异步aiohttp那套就完全是另一套用asyncio.Semaphore它不碰OS线程。