信号量(DispatchSemaphore vs AsyncSemaphore)、swift协作式线程池 and python信号量
通俗讲清楚 Task、挂起(suspend)、不阻塞内核线程
先把两个东西拆开对比,用你熟悉的GCD做参照物。
1、DispatchSemaphore(GCD老信号量)
底层操作对象:操作系统内核线程(OS Thread)
let sem = DispatchSemaphore(value:2)
sem.wait()
当计数为0,
wait()会调用操作系统内核,把当前正在跑的这条内核线程直接休眠、卡住。这条线程原地不动,什么活都干不了,内核把它调度出去。直到别的线程调用signal(),内核再把这条线程唤醒。
👉 代价:占住一条操作系统线程资源,干等,不能干别的。
如果并发网络请求很多,GCD为了不卡住,会不停新建更多内核线程 → 线程爆炸。
2、Swift Concurrency 的 Task + AsyncSemaphore(协程任务)
Task ≠ 操作系统内核线程!
Task 是Swift运行时在堆上分配的一个普通对象,保存:局部变量、代码执行到哪一行、状态(运行/挂起)。
- 你可以创建成千上万个Task,不会创建成千上万个内核线程。
- Swift有一个协作式线程池,线程数量基本等于CPU物理核数,比如手机8核,池子就8条工作内核线程,不会疯狂新增线程。
await = 挂起suspend(重点!不是block阻塞)
当执行到 await sem.wait()(AsyncSemaphore),许可用完的时候:
- 当前这个Task立刻被挂起。Swift运行时把这个Task所有现场(变量、执行位置)保存到堆内存。
- 把这条正在使用的内核线程还给线程池!释放出来!
- 这条内核线程马上拿去跑另外一个就绪的Task。
- 被挂起的Task就安安静静躺在内存队列里等待,它不占任何内核线程!
- 等到别的Task调用
signal()释放许可,这个等待的Task标记为就绪;线程池里面一旦有空闲内核线程,就拿过来继续跑这个Task剩下的代码。
重点:等待的时候,Task被挂起,内核线程已经被回收去干别的活,没有线程被卡住休眠。
这就是我说的:操作Task(用户态对象),不碰、不阻塞操作系统内核线程。
举个生活化比喻
-
DispatchSemaphore(GCD):会议室有2把椅子(许可)。来5个人(任务)。没椅子的人就原地站死等,人(内核线程)占住,啥别的活不干。人多了就不停招人(新建内核线程)来排队站着。
-
AsyncSemaphore(Swift Concurrency):会议室2把椅子。来5个人(Task)。没椅子的人全部坐到大厅等候区(堆内存队列),人离开工位(释放内核线程),工位给其他人用。椅子空出来,再叫大厅的人上来坐椅子干活。等候区坐1000个人都没事,不会占用工位。
大厅的人就是被挂起的Task,不占用工位(内核线程),只是内存对象。
上图逻辑:Task A走到await,挂起,释放线程;线程立刻跑Task C;等条件就绪,再拿一条空闲线程回来继续跑Task A。
3、非常关键的认知纠正
- Task本身不是线程!Task是一段要执行的作业,它只是临时借用线程池里面的内核线程跑一会儿;遇到
await挂起点,就把线程还回去。同一个Task前后执行,甚至可能跑在不同的内核线程上。 DispatchSemaphore.wait():阻塞内核线程(内核层面sleep)await AsyncSemaphore.wait():挂起Task(用户态Runtime管理),释放内核线程,内核完全不知道这个Task在等。
4、为什么GCD容易线程爆炸,Swift Concurrency可以开成千上万Task
GCD全局并发队列:只要有任务在wait()把线程阻塞住,GCD就会新建内核线程,保证后面任务有线程跑,越阻塞线程越多。
Swift Concurrency:Task遇到await就归还线程,不会占住线程死等,线程池数量固定等于CPU核数,不会无限创建线程,哪怕几百上千Task同时等待,也只消耗内存,不消耗内核线程资源。
5、和Python做个一一对应,打通你前面MinerU那套
| Swift | Python | 行为 |
|---|---|---|
| DispatchSemaphore | threading.Semaphore | 阻塞操作系统内核线程,必须多线程才有意义 |
| Task + AsyncSemaphore | 协程 + asyncio.Semaphore | 挂起Task/协程,释放OS线程;等待阶段不占用内核线程 |
MinerU第三方MCP:大量协程,
asyncio.Semaphore限流。成千上百协程对象,但是OS就一条主线程;等待网络的时候线程空闲,处理别的协程。和Swift Task+AsyncSemaphore逻辑一模一样。
6、一个高频踩坑(Swift)
❌千万不要在async函数内部调用GCD的
DispatchSemaphore.wait()
Task {
let sem = DispatchSemaphore(value:0)
sem.wait() // ❌灾难!直接把线程池的一条内核线程给阻塞占死,不归还!
}
这里wait()会把协作线程池的worker线程永久卡住,线程池可用线程变少,严重直接卡死整个并发运行时。
Swift async环境里面,要限流,就用
AsyncSemaphore,绝对不要用GCD信号量。
简短总结
用户态协程Task,不碰内核线程?
- Task是Swift运行时(用户态)维护的作业对象,保存在堆内存,不是操作系统内核线程。
- 当
await AsyncSemaphore.wait()拿不到许可,Task被挂起保存现场,把正在使用的内核线程归还线程池,线程可以去跑别的任务。 - 等待期间没有任何操作系统线程被休眠、阻塞,内核完全不知道这个Task在等待;全部逻辑在Swift Runtime(用户态)完成排队。
- 只有当Task被唤醒要继续执行的时候,才会再次“借”一条内核线程跑代码。
而老的DispatchSemaphore,直接调用操作系统内核,让真正的内核线程休眠卡住,这就是两者本质鸿沟。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)