iOS 中 GCD 与 多线程详解
一、 核心世界观:线程 vs 队列
在深入 GCD 之前,必须厘清两个层面的概念:

-
操作系统层面 (Kernel & Hardware):
-
线程 (Thread):CPU 调度的基本单位。创建、销毁、上下文切换(Context Switch)都有昂贵的系统开销(寄存器保存、栈切换、内核态用户态切换)。
-
多线程痛点:如果开启 1000 个线程,CPU 大部分时间都在做上下文切换,而不是执行代码。
-
-
GCD 层面 (User Space Library - libdispatch):
-
队列 (Queue):开发者用来组织任务的管道。
-
任务 (Block):一段要执行的代码闭包。
-
GCD 的魔法:GCD 维护了一个线程池 (Thread Pool)。它根据系统的负载(CPU 核数、内存占用),自动把队列里的任务分配给线程池中的线程去执行。
-
结论:Queue $\neq$ Thread。你只管把任务丢进队列,GCD 负责决定用哪个线程。
-
二、 GCD 的原子概念:同步/异步 & 串行/并发
这是面试和实战中最容易混淆的地方,必须用“行为”来定义:
1. 任务的执行方式 (Function)
决定了是否开启新线程,以及是否阻塞当前线程。
-
同步 (
dispatch_sync):-
不具备开启新线程的能力。
-
阻塞当前线程,直到 Block 执行完毕,代码才能继续往下走。
-
隐喻:你亲自去送信,送不到不回来。
-
-
异步 (
dispatch_async):-
具备开启新线程的能力(但不一定非要开,看队列类型)。
-
不阻塞当前线程,不用等 Block 执行完,代码直接往下走。
-
隐喻:你找了个快递员送信,你转头就去干别的事了。
-
2. 队列的类型 (Queue)
决定了任务的执行顺序。
-
串行队列 (Serial Queue):
-
一次只拿出一个任务执行,执行完这个才能拿下一个。
-
特点:严格有序,线程安全(单一线程访问)。
-
-
并发队列 (Concurrent Queue):
-
可以同时拿出多个任务执行。
-
特点:执行顺序不可预测,效率高,但需注意线程安全。
-
三、 深度排列组合与死锁 (OC 实例)
1. 经典的“死锁” (Deadlock)
场景:在主线程中,同步向主队列添加任务。
- (void)deadlockExample {
NSLog(@"1");
// 获取主队列(这是一个串行队列)
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// ❌ 崩溃/死锁发生处
dispatch_sync(mainQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
}
深刻解析:
-
主队列是串行队列:意味着任务必须一个接一个做。当前正在执行
deadlockExample这个任务。 -
dispatch_sync:意味着“把 Block 里的任务插队进来,并且我要立刻等到它执行完我才继续往下走”。
-
矛盾:主队列说:“你不执行完
deadlockExample,我没法腾出资源执行 Block”;dispatch_sync说:“你不执行完 Block,我不让deadlockExample继续”。 -
结果:互相等待,死锁。
2. 异步 + 串行 (Async + Serial)
场景:保证顺序,但不阻塞当前线程。
- (void)serialAsync {
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
NSLog(@"任务 %d - 线程: %@", i, [NSThread currentThread]);
});
}
}
底层行为:
GCD 会开启一条新线程(因为是异步,所以能开;因为是串行,所以只开一条够用了),任务严格按 0,1,2,3,4 顺序执行。
3. 异步 + 并发 (Async + Concurrent)
场景:火力全开,效率最高。
- (void)concurrentAsync {
dispatch_queue_t queue = dispatch_queue_create("com.demo.concurrent", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
NSLog(@"任务 %d - 线程: %@", i, [NSThread currentThread]);
});
}
}
底层行为:
GCD 会开启多条新线程(具体几条由 OS 决定)。任务开始顺序大致是 0,1,2,3,4,但结束顺序不确定。
四、 进阶:GCD 的高级调度 (Barrier & Group)
1. 读写分离锁 (The Reader-Writer Lock) - dispatch_barrier
这是 GCD 解决“多读单写”线程安全问题的优雅方案。
- 需求:读取数据可以并发(快),写入数据必须互斥(安全)。
@interface SafeDictionary : NSObject
@property (nonatomic, strong) NSMutableDictionary *dict;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@end
@implementation SafeDictionary
- (instancetype)init {
self = [super init];
_dict = [NSMutableDictionary dictionary];
// 必须自己创建并发队列
_concurrentQueue = dispatch_queue_create("com.demo.rw_queue", DISPATCH_QUEUE_CONCURRENT);
return self;
}
// 读取:并行 (Read)
- (id)objectForKey:(NSString *)key {
__block id obj;
// 使用 sync 是为了立刻拿到返回值,但在并发队列中,多个读操作可以同时进行
dispatch_sync(self.concurrentQueue, ^{
obj = self.dict[key];
});
return obj;
}
// 写入:栅栏 (Write)
- (void)setObject:(id)obj forKey:(NSString *)key {
// 栅栏函数:它执行时,队列里其他的任务(包括读和其他写)都被挡住,它是唯一执行者
dispatch_barrier_async(self.concurrentQueue, ^{
self.dict[key] = obj;
});
}
@end
原理:barrier 就像一个关卡。当队列执行到 barrier block 时,它会等待前面所有的并发任务完成,然后独占执行 barrier block,执行完后,再恢复后面的并发执行。
2. 任务依赖与通知 - dispatch_group
场景:先并发下载 3 张图片,等 3 张都下载完了,再合成一张大图。
- (void)groupExample {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 任务 A
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"下载图片 A");
dispatch_group_leave(group);
});
// 任务 B
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"下载图片 B");
dispatch_group_leave(group);
});
// 汇总通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有图片下载完毕,合成大图");
});
}
五、 深刻:从 Kernel (XNU) 视角看 GCD
要理解“深刻”,必须往下挖到内核层。
1. 线程池的本质:Workqueues
GCD 的底层并不直接操作 pthread_create 来频繁开关线程。它依赖于 XNU 内核提供的 Workqueue 机制。
-
当用户态的 GCD 队列中有任务时,它会请求内核。
-
内核的
workqueue子系统会维护一组线程,根据当前的 CPU 使用率决定是否唤醒现有线程或创建新线程。 -
优势:这是内核级支持的线程池,避免了用户态自己管理线程池的开销。
2. 优先级与 QoS (Quality of Service)
GCD 实际上将任务分级,映射到 CPU 的不同优先级。
-
User Interactive(UI 交互,优先级最高,对应主线程) -
User Initiated(用户急需结果,高优先级) -
Utility(耗时操作,低优先级) -
Background(用户不可见,最低优先级,甚至可能被 IO 节流)
优先级反转 (Priority Inversion):
如果一个高优先级的任务(如 UI)在等待一个低优先级的任务(如后台计算持有锁),GCD/内核会自动临时提升那个低优先级任务的等级,防止 UI 卡死。这就是优先级继承。
3. 信号量 (Semaphore) 的内核等待
dispatch_semaphore_wait 是非常高效的。
-
当信号量大于 0 时,它只是简单的原子操作(减 1),不进内核,极快。
-
只有当信号量为 0 需要等待时,它才会发起系统调用,让线程进入内核态休眠(主动让出 CPU),而不是忙等(Spin Lock)。
六、 iOS 中的锁
OSSpinLock 与 优先级反转 (The Trap of Speed)
OSSpinLock 曾被誉为 iOS 上性能最高的锁,因为它是一个自旋锁 (Spinlock)。
1. 自旋锁的本质 (User Space)
普通的锁(如 NSLock, pthread_mutex)如果获取不到锁,线程会进入休眠 (Sleep),这涉及到:
-
用户态 -> 内核态 的切换。
-
上下文切换 (Context Switch):保存当前寄存器,加载别的线程寄存器。
-
开销:这些操作非常昂贵(耗时)。
OSSpinLock 的逻辑是:如果锁被占用,我不睡,我在一个 while 循环里死循环(忙等,Busy-wait),一直问“锁好了吗?锁好了吗?”。
-
优势:如果等待时间极短,它避免了昂贵的上下文切换,性能极高。
-
劣势:忙等期间,CPU 是满负荷运转的(空转)。
2. 致命缺陷:优先级反转 (Priority Inversion)
在 iOS 9 之前,它活得很好。但随着 iOS 系统调度算法的优化(特别是引入 QoS 后),OSSpinLock 发生了严重的死锁问题。
场景复现:
-
低优先级线程 (Low Priority, LP) 先获取了锁,正在执行代码。
-
高优先级线程 (High Priority, HP) 来了,也想获取这个锁。
-
因为是自旋锁,HP 线程不会休眠,而是一直占用 CPU
while循环。 -
关键点:iOS 的调度器发现 HP 处于 Runnable 状态(它在忙等),于是把 CPU 时间片几乎全部分配给 HP。
-
结果:
-
HP 拿着 CPU 不干正事(在空转)。
-
LP 因为优先级低,抢不到 CPU 时间片,导致无法继续执行,也就无法释放锁。
-
造成死锁 (Livelock):HP 等 LP 放锁,LP 等 CPU 时间片(被 HP 抢走了)。
-
结论:
OSSpinLock在 iOS 10.0 中正式被标记为 Deprecated。
七、 os_unfair_lock (The Modern Hero)
苹果为了解决 OSSpinLock 的问题,推出了 os_unfair_lock(在 “ 中)。它是互斥锁,不是自旋锁。
1. 为什么叫 “Unfair” (不公平)?
这涉及到锁的唤醒策略。
-
公平锁:严格排队。线程 A 先来,线程 B 后来。锁释放时,一定是 A 先拿到。
-
不公平锁:锁释放时,如果刚好有个线程 C 过来抢,内核可能会把锁直接给 C,而不是唤醒还在睡的 A。
-
优势:唤醒沉睡的线程(A)需要上下文切换,成本高。直接给正在运行的线程(C)可以利用 CPU 缓存 (Cache Locality),大幅提升吞吐量。
-
代价:可能导致某些线程“饥饿”(迟迟抢不到锁),但在移动端这种微观场景下,性能收益远大于饥饿风险。
2. 它是如何解决优先级反转的?
os_unfair_lock 是系统内核级的锁。当高优先级线程等待锁时,它不会忙等,而是进入内核等待。
内核机制 - 优先级继承 (Priority Inheritance):
-
当 HP 线程被 os_unfair_lock 挡住时,内核识别到持有锁的是 LP 线程。
-
内核会临时将 LP 线程的优先级提升到和 HP 一样高。
-
这样 LP 就能拿到 CPU 时间片,尽快跑完代码释放锁。
-
锁释放后,LP 恢复原来的低优先级。
#import
os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
// Critical section
os_unfair_lock_unlock(unfairLock);
八、 @synchronized 底层实现 (The Magic & Heavyweight)
@synchronized(obj) 是 OC 程序员最爱用的语法糖,简单、安全,但它背后的实现相当复杂且“沉重”。我们深入 objc4 源码(objc-sync.mm)。
1. 核心问题:锁存在哪里?
你随便传一个 NSObject,它甚至没有专门的成员变量存锁,锁怎么和对象关联起来的?
答案:全局哈希表 (StripeMap)。
系统维护了一个全局的 Hash 表,里面存着很多锁。
2. 数据结构:StripeMap (条纹锁/分段锁)
系统并没有为每个对象分配一个锁(太费内存),也没有只用一个全局锁(性能太差)。而是使用了一个包含 64 个(不同系统可能不同)节点的数组,每个节点是一个链表。
// 伪代码逻辑
struct SyncList {
SyncData *data;
spinlock_t lock; // 用于保护链表的自旋锁
}
// 全局数组,大小通常是 16 或 64
static StripeMap sDataLists;
3. 核心流程 (objc_sync_enter)
当代码执行 @synchronized(obj) 时:
-
判空:如果
obj是nil,直接返回,不加锁。这就是为什么@synchronized(nil)不会崩溃但也锁不住。 -
Hash 映射:根据
obj的内存地址,计算 Hash 值,找到sDataLists中对应的那个槽位(Stripe)。 -
查找/创建 SyncData:
-
遍历该槽位的链表,看有没有已经关联
obj的SyncData节点。 -
如果有,引用计数 +1(支持递归锁)。
-
如果没有,创建一个新的
SyncData,由于obj的地址存进去,并挂载一个递归互斥锁 (recursive_mutex_t)。
-
-
加锁:对
SyncData中的mutex进行lock()。
4. 为什么说它性能较差?
-
查找开销:每次加锁都要把
obj地址 Hash,然后去全局表里遍历链表查找。 -
缓存不友好:这种全局查找容易导致 CPU Cache Miss。
-
内部锁:为了操作这个全局 Hash 表本身,内部还需要自旋锁来保证 Hash 表的线程安全。
5. 源码级简化流程图
Object Pointer (0x123456)
⬇
Hash Algorithm (计算哈希)
⬇
StripeMap Index (比如第 5 号槽位)
⬇
[SpinLock lock] (锁住第 5 号槽位的链表)
⬇
遍历链表 -> 找到/新建 (obj, recursive_mutex) 结构体
⬇
[recursive_mutex lock] (真正的加锁)
⬇
[SpinLock unlock]
⬇
执行你的代码块
StripeMap的底层实现

StripeMap 是 objc4 源码中 objc-sync.mm 文件里的核心数据结构,它的官方定义可以被称为:一种基于地址哈希的、条带化的、自旋锁保护的链表映射表。
为了让你彻底理解,我们分三个层次来拆解:设计哲学、数据结构、工作流程。
一、 设计哲学:为什么要搞这么复杂?
当你在 Objective-C 中写 @synchronized(obj) 时,Runtime 面临一个巨大的难题:把锁存在哪里?
-
方案 A:存在对象里
-
给每个
NSObject增加一个lock成员变量。 -
缺点:太浪费!App 里成千上万个对象,真正被
@synchronized锁住的可能只有几个。这会极大地增加所有对象的内存体积。
-
-
方案 B:存在一个全局 Map 里
-
搞一个全局
Dictionary。 -
缺点:太慢!这意味着全 App 所有的
@synchronized都要去抢同一把保护这个 Dictionary 的锁。多线程并发时,这里会成为超级瓶颈。
-
方案 C:StripeMap (分段锁/条带化) 这是 Runtime 的选择。它把“一把全局大锁”拆成了“N 把局部小锁”。
-
它不像方案 A 那样浪费内存(只存储被锁的对象)。
-
它不像方案 B 那样竞争激烈(不同的对象可能落在不同的“条带”里,互不干扰)。
二、 核心数据结构:解剖 StripeMap
请把 StripeMap 想象成一个只有 64 个抽屉的柜子(在真机上通常是 64,模拟器可能是 8 或 16)。
1. 顶层结构:固定数组
// 简化后的伪代码逻辑
class StripeMap {
enum { StripeCount = 64 }; // 条带数量,通常是 64
struct SyncList array[StripeCount]; // 静态数组
// 根据对象地址计算去哪个抽屉
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = (uintptr_t)p;
// 核心哈希算法:位移去零 + 取模
return ((addr >> 5) ^ (addr >> 9)) % StripeCount;
}
}
2. 中层结构:抽屉 (SyncList)
每个“抽屉”里存放着两样东西:
-
一把自旋锁 (SpinLock):只为了保护当前这个抽屉里的链表操作(增删查)。
-
一个链表头指针 (data):指向具体的锁数据。
struct SyncList {
SyncData *data; // 链表头
spinlock_t lock; // 保护这个链表的自旋锁(极其轻量)
};
3. 底层结构:真正的锁 (SyncData)
这才是真正存放“对象”和“递归锁”的地方。
struct SyncData {
struct SyncData* nextData; // 指向下一个节点(链表结构)
const void* object; // 被锁的对象 (key)
// 真正的锁!也就是 @synchronized 等待的那把锁
recursive_mutex_t mutex;
int threadCount; // 多少个线程在使用它
};
三、 完整工作流程:从 @synchronized(obj) 开始
假设你有两个对象 objA 和 objB,以及两个线程。
场景 1:无冲突(理想情况)
-
计算 Hash:
- 线程 1 对
objA加锁。Runtime 算出objA对应 第 3 号抽屉。
- 线程 1 对
-
锁住抽屉:
- Runtime 获取 第 3 号抽屉 的
SyncList.lock(自旋锁)。因为操作链表非常快,这个锁持有时间极短。
- Runtime 获取 第 3 号抽屉 的
-
查找/创建:
-
遍历第 3 号抽屉的链表。发现是空的。
-
创建一个新的
SyncData节点,把objA和一把新的recursive_mutex存进去。 -
把这个节点挂到链表上。
-
-
解锁抽屉:
- 释放 第 3 号抽屉 的自旋锁。其他线程现在可以访问第 3 号抽屉了。
-
真正加锁:
-
Runtime 对
SyncData里的mutex(递归互斥锁) 进行lock()。 -
注意:这才是你的代码块开始执行、别的线程会被阻塞的地方。
-
场景 2:哈希冲突(StripeMap 的精髓)
假设 objB 的地址经过计算,居然也对应 第 3 号抽屉(虽然概率只有 1/64,但会发生)。
-
锁住抽屉:
-
线程 2 想锁
objB。它尝试获取 第 3 号抽屉 的自旋锁。 -
如果此时线程 1 刚好在第 3 号抽屉里创建节点(步骤 3),线程 2 会短暂忙等(Spin)。但因为步骤 3 只是内存操作,极快,所以几乎瞬间就能拿到。
-
-
遍历链表:
-
线程 2 拿到了抽屉锁。遍历链表,发现了
objA的节点,但地址不匹配。 -
继续找,没找到。
-
创建
objB的SyncData节点,挂在objA节点的后面。
-
-
解锁抽屉:
- 释放抽屉锁。
-
真正加锁:
-
线程 2 对
objB的mutex进行lock()。 -
关键点:虽然
objA和objB在同一个“抽屉”里,但它们拥有独立的mutex。所以线程 1 锁objA完全不会阻塞线程 2 锁objB。它们可以并行执行!
-
四、 性能优化的极致:TLSCache (线程局部缓存)
你可能会问:“每次加锁都要去查全局表、抢抽屉锁、遍历链表,这不是很慢吗?”
没错。所以 Apple 在 StripeMap 之上加了一层 TLSCache (Thread Local Storage Cache)。
逻辑如下:
-
当线程第一次锁
objA时,走上面的完整流程。 -
拿到
SyncData后,Runtime 会把这个SyncData的指针偷偷存到当前线程的 TLS(线程私有数据区) 里。 -
第二次锁同一个对象时(比如递归调用,或者同一个线程短时间内多次锁):
-
Runtime 先去读 TLS。
-
发现:“咦,我刚才好像锁过这个
objA,缓存里有SyncData”。 -
直接跳过
StripeMap的查找过程,直接对缓存里的mutex引用计数 +1。 -
性能爆炸:完全没有全局竞争,变成了纯粹的线程内部操作。
-
五、 StripeMap 总结(面试话术)
如果面试官问你 @synchronized 的 StripeMap,你可以这样总结:
-
定义:它是一个分段锁哈希表,用于管理对象与锁的映射关系。
-
结构:它包含一个固定大小(如 64)的数组,数组元素是链表。
-
双层锁设计:
-
外层有 SpinLock 保护链表本身的增删查(极短时间的自旋)。
-
内层有 RecursiveMutex 真正保护用户的代码逻辑(长时间的阻塞)。
-
-
解决的问题:通过 Hash 将竞争分散到 64 个 Bucket 中,解决了全局大锁的性能瓶颈,同时避免了给每个对象增加额外内存消耗。
-
优化:配合 TLS (线程局部存储) 缓存最近使用的锁,在递归或高频加锁场景下,能绕过 StripeMap 查找,实现极高效率。
一张图总结:
Plaintext
Object Addr -> Hash() -> Index 3
⬇
[ Stripe 3 (SpinLock) ]
⬇
Link List: Node A -> Node B -> Node C
⬆
Object: objB
Lock: RecursiveMutex (真正干活的锁)
自旋锁不是被废弃了吗 苹果还在用吗?
这是一个非常敏锐且专业的问题!你发现了源码命名与官方文档之间的“矛盾”。
一句话答案: OSSpinLock(那个 API)确实被彻底废弃了,甚至被视为“毒药”。 但是,在 objc4 源码里你看到的 spinlock_t,只是披着“自旋锁”名字的马甲,它的内核其实已经偷偷换成了 os_unfair_lock。
我们深入源码(objc-os.h)来把这个“案子”破了。
一、 源码里的“挂羊头卖狗肉”
如果你打开 Apple 开源的 objc4-818 (或更新版本) 的源码,找到 objc-os.h 文件,你会看到这样的定义:
// 简化后的源码逻辑展示
#if defined(__APPLE__) && !defined(OS_UNFAIR_LOCK_INLINE)
// ... 一些环境判断
#endif
// 重点在这里!!!
using spinlock_t = mutex_tt;
再往下挖 mutex_tt 的实现:
template
class mutex_tt : nocopy_t {
os_unfair_lock mLock; // <--- 凶手找到了!它里面包的是 os_unfair_lock
public:
void lock() {
// 调用的是 os_unfair_lock_lock
os_unfair_lock_lock(&mLock);
}
void unlock() {
os_unfair_lock_unlock(&mLock);
}
// ...
};
真相大白: Runtime 内部虽然变量名叫 spinlock_t,虽然注释里可能还写着 Spinlock,但它底层调用的全是 os_unfair_lock。
二、 为什么要保留 spinlock_t 这个名字?
这更多是历史包袱和语义表达的原因:
-
历史原因:Objective-C Runtime 的代码历史非常悠久。以前它确实用的是真的自旋锁。为了不重构几千行代码里的类型声明,苹果工程师选择直接修改
spinlock_t的定义,把底层替换掉。 -
语义原因:在
StripeMap这种场景下,开发者意图是“极短时间的锁定”。叫它spinlock能提醒维护者:“这里只能做极简单的内存操作,千万别在这里面做耗时操作(比如 IO)!”
三、 真的完全不“自旋”了吗?
这也是一个深度的细节。os_unfair_lock 虽然是互斥锁(会休眠),但它在内核层面的实现非常智能。
-
纯粹的 OSSpinLock (已死):
- 获取失败 ->
while(1)死循环空转 -> 浪费 CPU -> 导致优先级反转。
- 获取失败 ->
-
现在的 os_unfair_lock:
- 获取失败 -> 可能会在用户态尝试几次(Bounded Spin,有限自旋) -> 还没拿到? -> 立即进入内核休眠 (Wait)。
苹果的策略是: 既然 StripeMap 的操作仅仅是“从链表里取个节点”这么快(纳秒级),那么使用 os_unfair_lock 带来的开销(即使发生内核休眠)也是完全可以接受的,而且彻底解决了优先级反转造成的死锁风险。
四、 总结
-
官方态度:
OSSpinLock确实不能用了,谁用谁 Crash(死锁)。 -
内部实现:
@synchronized内部的StripeMap依然在使用名为spinlock_t的类型,但它不是OSSpinLock。 -
实质:它是封装了
os_unfair_lock的 C++ 类。
所以,Apple 并没有打脸,它只是通过一次“偷天换日”的底层替换,既保留了代码结构,又修复了致命 Bug。
深度总结与对比
| 锁类型 | 底层实现 | 优先级反转 | 性能 | 特点 |
|---|---|---|---|---|
| OSSpinLock | while 忙等 (用户态) | 严重 (导致死锁) | 极高 (曾是) | 已废弃。仅适用于极短临界区。 |
| os_unfair_lock | 互斥锁 (内核态等待) | 解决 (优先级继承) | 很高 | OSSpinLock 的官方替代品,不保证 FIFO,C 语言 API。 |
| pthread_mutex | 互斥锁 (Unix 标准) | 无特殊处理 | 高 | 跨平台通用,支持递归/非递归配置。 |
| NSLock | 封装 pthread_mutex | 同上 | 中 | OC 对象封装,使用方便。 |
| @synchronized | 全局 Hash 表 + 递归锁 | 无特殊处理 | 低 | 语法最简单,支持递归,自动处理异常解锁,性能最差但一般场景够用。 |
架构师视角的建议:
-
追求极致性能:使用
os_unfair_lock。 -
跨平台/通用 C++ 混编:使用
std::mutex或pthread_mutex。 -
业务层快速开发:
@synchronized依然是首选,除非 instruments 分析出它是瓶颈(99% 的 UI 业务逻辑中,锁的开销不是瓶颈)。 -
面试加分项:提到
@synchronized的 StripeMap 设计,以及OSSpinLock死锁的 QoS 调度背景。
九、 总结与建议
-
心法:不要思考“线程”,要思考“任务”和“队列”。
-
死锁铁律:永远不要在当前串行队列中同步 (
sync) 添加任务到本队列。 -
性能优化:
-
尽量使用
DISPATCH_QUEUE_CONCURRENT配合barrier来处理资源竞争(比@synchronized高效)。 -
控制并发数量。如果你用
dispatch_apply遍历一个 10000 次的数组,GCD 可能会尝试开几十个线程,导致内存爆炸(Thread Explosion)。此时应该配合信号量控制并发数。
-
GCD 是 iOS 开发者的内功。理解了它,你实际上就理解了现代操作系统如何高效地处理并行计算。