Python两套原生信号量,正好一一对应iOS两套

Python标准库就两套信号量,名字一模一样,但是完全不能混用。

  1. threading.Semaphore → 完全等价 iOS GCD DispatchSemaphore

操作对象:操作系统内核线程
sem.acquire():拿不到许可,直接把当前OS线程阻塞休眠,线程原地卡住,不归还;别的线程调用release()才唤醒这条线程。
✅必须多操作系统线程才有意义;单线程调用会卡死死锁。
搭配:threading.Thread / ThreadPoolExecutor(同步多线程)。

  1. asyncio.Semaphore → 完全等价 iOS15+ Swift Concurrency AsyncSemaphore

操作对象:协程Task(用户态对象),不碰内核线程
await sem.acquire():拿不到许可,挂起协程,把当前占用的OS线程还给事件循环,线程去跑别的协程;等待期间不占线程。
✅跑在单OS线程事件循环,不需要开多内核线程。
搭配:async def协程、aiohttp,就是MinerU MCP用的那套。

补充还有 BoundedSemaphore,分别在threadingasyncio下面,只是防止过度release,底层行为不变。

一一映射对照表

Python iOS 行为 是否阻塞OS内核线程
threading.Semaphore DispatchSemaphore(GCD) 线程信号量 ✅会阻塞休眠OS线程
asyncio.Semaphore AsyncSemaphore(Swift Concurrency) 协程Task信号量 ❌只挂起Task,释放线程

重点坑(和iOS一模一样)

  1. 千万不要在async协程里面用threading.Semaphore
    就像Swift Task里面不要调用DispatchSemaphore.wait()
# 灾难写法!
async def bad():
    sem = threading.Semaphore(0)
    sem.acquire() # 直接把事件循环那一条OS主线程阻塞卡死,全部协程冻住

threading.Semaphore.acquire()是同步阻塞调用,没有await,直接卡住内核线程,整个asyncio事件循环卡死。

  1. asyncio.Semaphore线程不安全,不能跨不同操作系统线程使用,只能在同一个事件循环内部协程之间用,这点也和Swift的AsyncSemaphore一致。

结合MinerU的场景复盘

  1. 如果你写同步requests + ThreadPoolExecutor(max_workers=3)
    • 底层是多条OS线程;
    • 限流优先用max_workers,也可以叠加threading.Semaphore,两者都是操作OS线程。
  2. 如果你写aiohttp异步协程(第三方MinerU‑MCP)
    • 单OS线程,大量协程对象;
    • asyncio.Semaphore(2‑4)做闸门,协程挂起、线程归还,不会线程爆炸。

python也有有类似于iOS GCD全局队列那种信号量
就是threading.Semaphore,就是GCD DispatchSemaphore的Python复刻,作用于操作系统线程,acquire会把线程阻塞休眠,必须多线程才能干活。

极简示例,GCD等价的Python threading.Semaphore

from threading import Thread, Semaphore
import time

sem = Semaphore(2) # 最多2个并发,等价DispatchSemaphore(value:2)

def worker(i):
    sem.acquire()   # 许可耗尽,当前OS线程直接阻塞休眠
    print(f"线程{i}运行")
    time.sleep(3)
    sem.release()

for i in range(10):
    Thread(target=worker, args=(i,)).start()

现象:会创建多条操作系统线程;拿不到许可的线程直接内核休眠,GCD那种线程爆炸的问题,Python这里同样会出现。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐