通俗讲清楚 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),许可用完的时候:

  1. 当前这个Task立刻被挂起。Swift运行时把这个Task所有现场(变量、执行位置)保存到堆内存。
  2. 把这条正在使用的内核线程还给线程池!释放出来!
  3. 这条内核线程马上拿去跑另外一个就绪的Task。
  4. 被挂起的Task就安安静静躺在内存队列里等待,它不占任何内核线程
  5. 等到别的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、非常关键的认知纠正

  1. Task本身不是线程!Task是一段要执行的作业,它只是临时借用线程池里面的内核线程跑一会儿;遇到await挂起点,就把线程还回去。同一个Task前后执行,甚至可能跑在不同的内核线程上。
  2. DispatchSemaphore.wait()阻塞内核线程(内核层面sleep)
  3. 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,不碰内核线程?

  1. Task是Swift运行时(用户态)维护的作业对象,保存在堆内存,不是操作系统内核线程。
  2. await AsyncSemaphore.wait()拿不到许可,Task被挂起保存现场,把正在使用的内核线程归还线程池,线程可以去跑别的任务
  3. 等待期间没有任何操作系统线程被休眠、阻塞,内核完全不知道这个Task在等待;全部逻辑在Swift Runtime(用户态)完成排队。
  4. 只有当Task被唤醒要继续执行的时候,才会再次“借”一条内核线程跑代码。

而老的DispatchSemaphore,直接调用操作系统内核,让真正的内核线程休眠卡住,这就是两者本质鸿沟。

Logo

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

更多推荐