1. 线程是谁创建的?
结论:线程是由系统内核(XNU Kernel)通过 pthread 库创建的,而不是由 DispatchQueue 队列直接创建。
GCD 并不拥有线程,它拥有的是请求线程的权利。
- 当你向并行队列(Concurrent Queue)提交任务时,GCD 的底层组件
libdispatch会根据当前的系统负载、CPU 核心数以及任务的优先级(QoS),向系统内核发送信号。 - 内核接收到信号后,会决定是分配一个现有的空闲线程,还是通过
pthread创建一个新线程来响应请求。
2. GCD 管理线程池的层级结构
GCD 的线程管理是一个典型的“生产者-消费者”模型,分为三个层级:
A. 用户层队列 (User Queues)
也就是我们代码里创建的 DispatchQueue.main 或自定义队列。这些队列负责存储任务,但不负责执行。
B. 根队列 (Root Queues)
这是系统预设的一组全局队列(Global Queues),它们与不同的 QoS (Quality of Service) 对应。所有的用户队列最终都会指向(Target)这些根队列。
Background,Utility,Default,UserInitiated,UserInteractive。
C. 工作池 (Workload / Thread Pool)
这是最底层。libdispatch 内部维护了一个管理线程状态的数据结构。它会监控根队列中的任务堆积情况,并与内核通信来调节工作线程的数量。
3. 线程池的“伸缩”逻辑:如何管理?
GCD 并不是简单地维持固定数量的线程,它的管理遵循以下逻辑:
- 动态扩展:当并行队列中任务很多,且 CPU 仍有空余处理能力时,GCD 会请求内核增加线程。
- 上限控制:为了防止系统资源枯竭,GCD 会为一个进程设置线程总数限制(通常在 64 个 左右)。一旦达到上限,即使你继续异步提交任务,GCD 也不会再创建新线程。
- 闲置销毁:如果一个线程在一段时间内(通常是几十秒)没有接到新任务,GCD 会允许内核将其回收,以节省内存资源。
- 优先级映射:GCD 会将任务的 QoS 映射到
pthread的调度优先级上。这意味着UserInteractive任务所在的线程会比Background任务获得更多的 CPU 时间片。
4. 关键机制:协同调度 (Cooperative Scheduling)
在传统的多线程模型中,线程之间是竞争关系。而 GCD 通过 dispatch_root_queue 实现了一种协同管理:
- 反馈回路:GCD 会感知当前 CPU 的活跃程度。如果系统正在发热或电池电量极低,它会主动放缓新线程的创建速度。
- 上下文切换优化:GCD 尽量让同一个线程处理同一个队列中的连续任务,这样可以利用 CPU 的 L1/L2 缓存局部性,减少昂贵的上下文切换。
5. 常见陷阱:为什么会“死锁”或“爆炸”?
- 死锁 (Deadlock) :当你向当前正在执行的串行队列同步提交一个任务(
sync),该队列会被阻塞等待自己完成,但由于它被占用,任务永远无法开始。 - 线程爆炸 (Thread Explosion) :如果你在并行队列里提交了大量耗时且阻塞(如同步读取磁盘)的任务,GCD 会认为当前线程都卡住了,于是不断申请新线程来尝试消耗队列。这会导致线程数迅速触达上限,造成内存激增和频繁的上下文切换。
💡 深度启发:Swift 并发 (Swift Concurrency) 的进化
为了彻底解决 GCD 的“线程爆炸”问题,Swift 5.5 引入的新并发模型采用了一种 “协作式线程池” 。它保证线程的总数永远不超过 CPU 的核心数。当一个任务遇到 await 挂起时,它会交出线程让给其他任务,而不是阻塞线程去申请新的。