一、前言:告别API背诵,吃透GCD底层本质
在iOS开发中,GCD(Grand Central Dispatch)是日常开发使用频率最高、底层最核心的多线程方案,几乎所有异步任务、主线程刷新、延时执行、任务调度都依赖GCD。但绝大多数开发者仅停留在API调用层面,只会死记硬背「串行有序、并发无序、async开线程、sync阻塞线程」,遇到复杂场景频繁踩坑:
-
明明是并发队列,任务却串行执行,找不到原因
-
dispatch_sync 死锁场景只会背诵结论,不懂底层阻塞原理
-
不清楚GCD线程池上限,大量任务并发导致卡顿、线程爆炸
-
分不清队列、线程、任务三者的绑定关系,面试高频提问无从作答
-
不了解Block在GCD中的封装、调度、执行、销毁全流程
GCD 并非苹果高层封装的黑盒,其完全开源,底层基于libdispatch纯C语言实现,核心围绕「队列管理、线程池调度、任务封装执行」三大核心模块。
本文将基于GCD源码,从零拆解:队列底层结构体、串行/并发队列核心差异、GCD线程池复用机制、Block任务封装与执行全链路、同步/异步调度底层逻辑,搭配超多实战案例、踩坑代码、对比演示,彻底打通GCD从原理到落地的所有知识点。
二、GCD整体架构:三大核心角色(源码视角)
GCD 整体架构极简,所有调度逻辑都围绕 任务(Block)、队列(Queue)、线程池(Thread Pool) 三者联动,和日常认知完全不同:GCD不直接管理线程,只管理队列和任务,线程由系统线程池统一调度。
1. 三大核心角色定义(源码本质)
-
任务(Dispatch Continuation):我们传入的Block不会直接执行,底层被封装为
dispatch_continuation_t结构体,记录任务内容、优先级、所属队列、Group信息、回调状态等,是GCD最小执行单元 -
队列(dispatch_queue_t):底层是FIFO双向链表,负责有序接收、缓存任务,区分串行/并发类型,只存任务,不执行任务
-
线程池(Worker Thread):系统全局复用的线程池,GCD从线程池中获取空闲线程,执行队列中的任务,任务执行完毕后线程归还池内复用
2. 完整调度链路(源码执行流程)
开发者提交Block任务 -> 封装为continuation任务单元 -> 加入对应队列链表 -> GCD调度线程池空闲线程 -> 线程执行任务 -> 任务完成,线程归还池内复用
核心结论:队列是「任务容器」,线程池是「执行载体」,Block是「执行内容」,三者完全解耦,这也是GCD高效、低开销的核心原因。
三、队列源码深度解析:串行/并发/主队列/全局队列
队列是GCD的核心载体,所有任务必须提交到队列才能执行。很多人混淆队列类型、执行规则、线程特性,本质是不懂队列底层结构体标识。
1. 队列底层核心结构体(精简源码)
GCD所有队列都是 dispatch_queue_s 结构体对象,核心通过 dq_width 字段区分串行/并发队列:
// GCD底层队列核心结构体(开源源码精简)
struct dispatch_queue_s {
// 队列基础对象信息
struct dispatch_object_s *_do;
// 队列最大并发数(核心标识)
uint32_t dq_width;
// 队列待执行任务链表
LIST_HEAD(, dispatch_continuation_s) dq_items;
// 线程池调度相关属性
struct dispatch_thread_pool_s *dq_thread_pool;
// 队列优先级、标签、标识等
char *dq_label;
};
2. 四类队列核心源码特性对比
|
队列类型 |
dq_width 值 |
底层特性 |
线程来源 |
执行规则 |
|---|---|---|---|---|
|
自定义串行队列 |
1 |
最大并发数为1,同一时间仅1个任务执行 |
线程池空闲线程(可复用) |
FIFO有序执行,任务串行排队 |
|
自定义并发队列 |
UINT32_MAX |
最大并发数无上限(受系统线程池限制) |
线程池多线程 |
任务并发执行,无序完成 |
|
主队列 MainQueue |
1 |
特殊串行队列,绑定主线程 |
唯一主线程,无复用 |
主线程有序执行UI任务 |
|
全局并发队列 GlobalQueue |
UINT32_MAX |
系统全局共享并发队列,自带优先级 |
系统全局线程池 |
全局任务并发调度 |
3. 队列创建源码与实战示例
队列创建API dispatch_queue_create 底层会初始化结构体,根据第二个参数赋值 dq_width:
// 源码逻辑:DISPATCH_QUEUE_SERIAL = 1,DISPATCH_QUEUE_CONCURRENT = 最大值
// 1. 自定义串行队列(dq_width = 1)
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
// 2. 自定义并发队列(dq_width = UINT32_MAX)
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 3. 全局并发队列(自带优先级:高/默认/低/后台)
dispatch_queue_t globalQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
// 4. 主队列(全局单例,不可创建)
dispatch_queue_t mainQueue = dispatch_get_main_queue();
4. 队列核心误区实战验证
误区:串行队列 = 只有一条线程,并发队列 = 多条线程
源码真相:串行队列只是同一时间只执行1个任务,底层会复用多条线程,只是任务串行排队,不会并发执行。
// 串行队列多任务线程打印验证
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 5; i++) {
dispatch_async(serialQueue, ^{
NSLog(@"任务%d:线程%@", i, [NSThread currentThread]);
});
}
打印结果:5个任务有序执行,但线程ID不完全一致,证明串行队列会复用线程池线程,并非固定单线程。
四、GCD线程池源码级解析:线程复用、上限、调度规则
GCD的高效性,核心依托系统全局线程池,这也是GCD优于手动NSThread的关键。很多人疑惑「GCD最多开多少线程、为什么任务多了会卡顿、线程如何复用」,答案全在线程池源码逻辑中。
1. GCD线程池底层原理
GCD 不单独为某个队列创建线程,所有自定义队列、全局队列的异步任务,均由系统全局统一线程池调度:
-
线程池采用「懒创建、自动复用、空闲回收」机制
-
任务提交后,线程池优先分配空闲线程,无空闲则新建线程
-
线程执行完任务后不销毁,回归线程池等待复用,减少线程创建销毁开销
-
系统限制线程池最大线程数,iOS设备默认上限64条(不同版本略有差异),超出上限任务排队等待
2. 同步/异步与线程池的绑定关系(核心)
(1)dispatch_async 异步调度
不会阻塞当前线程,向线程池申请线程执行任务,具备开启新线程能力,执行逻辑:
-
串行异步队列:线程池分配线程,任务串行执行(同一时间1个任务)
-
并发异步队列:线程池分配多条空闲线程,任务同时并发执行
(2)dispatch_sync 同步调度
不向线程池申请新线程,直接占用当前调用线程执行任务,全程阻塞当前线程,任务执行完毕才返回,这也是同步死锁的底层根源。
3. 线程池上限实战案例(线程爆炸问题)
当并发队列异步任务远超线程池上限时,不会无限创建线程,多余任务会在队列排队,等待线程复用,避免线程爆炸崩溃:
// 批量提交100个并发任务,验证线程池上限
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 100; i++) {
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1.0]; // 模拟耗时任务
NSLog(@"任务%d执行,线程:%@", i, [NSThread currentThread]);
});
}
现象:同时执行的任务数稳定在60+,不会持续新增线程,多余任务排队,线程反复复用,完美规避线程爆炸。
五、Block任务源码级执行全链路:封装、调度、执行、销毁
我们传入的 Block 并非直接交给线程执行,GCD 底层有一套完整的任务封装与调度机制,这也是GCD能统一管理所有任务的核心。
1. Block底层封装:从代码到dispatch_continuation_t
源码中,所有GCD提交的Block,都会被包装为 dispatch_continuation_t 结构体(任务延续体),核心存储:
-
原始Block函数指针、参数、返回值
-
任务所属队列、优先级、QOS等级
-
任务状态:待执行、执行中、已完成、已取消
-
关联的Group、信号量、回调信息
包装完成后,该结构体对象会被加入队列的FIFO链表,等待线程池调度执行。
2. 任务完整执行流程(源码时序)
-
封装阶段:Block 封装为 dispatch_continuation_t 任务单元
-
入队阶段:任务按FIFO规则插入队列链表尾部
-
调度阶段:GCD内核唤醒线程池空闲线程
-
出队阶段:线程从队列头部取出任务单元
-
执行阶段:线程执行Block内部代码
-
销毁阶段:任务执行完毕,释放continuation结构体,线程回归池内复用
3. 同步/异步任务执行差异案例(精准对比)
案例1:串行队列 + 异步任务
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"任务开始");
dispatch_async(serialQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"异步任务1完成");
});
dispatch_async(serialQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"异步任务2完成");
});
NSLog(@"任务结束");
执行结果:不阻塞主线程,任务1执行完再执行任务2,串行有序,主线程立即执行「任务结束」。
源码原理:async开启新线程,队列dq_width=1,任务排队执行,不阻塞当前主线程。
案例2:串行队列 + 同步任务
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"任务开始");
dispatch_sync(serialQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"同步任务1完成");
});
dispatch_sync(serialQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"同步任务2完成");
});
NSLog(@"任务结束");
执行结果:阻塞主线程,按顺序执行任务1、任务2,最后打印「任务结束」。
源码原理:sync占用主线程执行任务,队列串行规则生效,全程阻塞调用线程。
案例3:并发队列 + 异步任务(真正并发)
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"任务开始");
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"并发任务1完成");
});
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"并发任务2完成");
});
NSLog(@"任务结束");
执行结果:不阻塞主线程,先打印任务2(耗时短),后打印任务1,任务无序并发执行。
源码原理:并发队列dq_width无上限,线程池分配多条线程同时执行任务。
六、GCD死锁源码级解析:彻底读懂死锁本质
死锁是GCD最高频面试、开发踩坑点,所有死锁的唯一底层原因:当前线程被同步任务阻塞,且等待当前队列的任务执行完成,形成循环等待。
1. 经典死锁案例(主队列同步死锁)
// 主线程、主队列执行同步任务
NSLog(@"开始死锁测试");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"主队列同步任务");
});
NSLog(@"代码永远执行不到这里");
2. 源码层级死锁拆解
-
当前代码在主线程+主队列执行
-
dispatch_sync阻塞主线程,等待Block任务执行完毕 -
Block任务被加入主队列链表尾部
-
主队列是串行队列,需要等待当前正在执行的代码完成,才能执行尾部Block
-
主线程被sync阻塞等待Block,Block等待主线程代码执行完成,循环等待,永久死锁
3. 非主队列死锁案例(自定义串行队列嵌套同步)
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"外层任务");
// 嵌套同步当前队列任务,触发死锁
dispatch_sync(serialQueue, ^{
NSLog(@"内层任务");
});
});
死锁原理:外层任务占用串行队列,同步阻塞等待内层任务,内层任务排队等待外层任务完成,循环阻塞死锁。
核心结论:只要在当前串行队列的任务中,同步嵌套当前队列任务,必死锁,与是否是主队列无关。
七、高频实战场景+源码级答疑
1. 为什么全局并发队列同步任务不会并发?
sync 不申请新线程,占用当前线程依次执行任务,即便队列是并发类型,也无法实现并发,本质是执行线程唯一,和队列类型无关。
2. 异步串行队列为什么偶尔多条线程执行?
串行队列仅限制任务执行时序(FIFO),不限制线程数量,GCD线程池会动态复用线程,只要不同时执行多个任务,就符合串行规则。
3. GCD线程池会不会无限创建线程?
不会,系统内核限制最大线程数,超出上限任务排队,等待线程复用,从底层杜绝线程爆炸、OOM闪退问题。
4. 主队列和普通串行队列的核心区别?
普通串行队列复用线程池线程,主队列独占唯一主线程,所有UI任务必须在主队列执行,且主线程不参与线程池复用。
八、企业级GCD使用规范(基于源码特性)
-
耗时任务统一使用全局并发队列异步执行:规避主线程卡顿,复用系统线程池,无需手动管理线程
-
数据读写、资源加锁使用自定义串行队列:利用串行队列单任务执行特性,实现线程安全,替代笨重的互斥锁
-
禁止当前串行队列嵌套sync任务:从源头杜绝死锁问题
-
大量并发任务控制数量:避免瞬间提交上千任务,导致队列排队、调度卡顿
-
优先使用async,少用sync:sync阻塞线程,极易引发性能问题和死锁,仅用于极简同步同步场景

1万+

被折叠的 条评论
为什么被折叠?



