多线程模型下,由于共享内存带来的冲突风险,锁是个避不开的话题。
关于锁
首先从平台无关的角度看,从能力上区分,主要有以下几种锁:
- 互斥锁(mutex):最普通的锁,阻塞等待,一种二元锁机制,只允许一个线程进入临界区
- 自旋锁(spin lock):能力上跟互斥锁一样的,只是它是忙等待
- 信号量(semaphore):信号量可以理解为互斥锁的推广形态,即互斥锁取值 0 /1,而信号量可以取更多的值,从而应对更复杂的同步。
- 条件锁(condition lock):有时候互斥的条件是复杂的而不是简单的数量上的竞争,此时可以用条件锁,条件锁的加锁解锁是通过代码触发的。
- 读写锁(read-write lock):顾名思义,就像文件读写,读操作之间并不互斥,但写操作与任何操作互斥。
- 递归锁(recursivelock):互斥锁的一个特例,允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
所有的锁,语义上基本就这几种,
iOS 中的锁
以 API 提供者的维度梳理一下 iOS 的锁
-
内核
- OSSpinLock:内核提供的自旋锁,已废弃
- os_unfair_lock:iOS10 后官方推荐用来替代 OSSpinLock 的方案,性能很好
-
pthread:POSIX 标准真的是大而全 … 啥都有
- pthread_mutex:pthread 的互斥锁
- pthread_rwlock:pthread 的读写锁
- pthread_cond_t:pthread 的条件锁
- sem_t:pthread 的信号量
- pthread_spin_lock:pthread 的自旋锁
-
GCD
- dispatch_semaphore:gcd 的信号量
-
Cocoa Foundation
- NSLock:CF 的互斥锁
- NSCondition:条件变量
- NSConditionLock:条件锁,在条件变量之上做了封装
- NSRecursiveLock
-
objc runtime
- synchronized:本质上是 pthread_mutex 的上层封装,参考这里
以上,相对全面地列举了 iOS 中的锁,它们是不同层级的库提供的,但由于 iOS 中所有的线程本质上都是内核级线程,因此这些锁是能够公用的。
- 串行队列
- dispatch_barrier_async:栅栏函数,隔离前后的任务
- atomic
性能对比
环境:iPhone 7 plus + iOS 11
基于 YY 老师 不再安全的 OSSpinLock 中的性能对比代码,加入了os_unfair_lock
,重新跑的一个性能对比。测试代码在这里
上面测试的是纯粹的加锁解锁性能,中间没有任何逻辑也不存在多线程抢占,为了更贴合我们的实际环境,我构造了一个简单的多线程环境:NSOperationQueue
最大并发数为 10,创建 10 个 NSOperation
,每个NSOperation
做 10w 次 i ++ 操作,每次操作加锁,代码在这里,结果如下:
可以看到多线程抢占的情形下结果跟前面略有不同,在真实业务场景下这个数据应该更有参考意义。
如何选择
由于 OSSpinLock 存在的优先级反转问题,已经废弃不再使用。(参考:不再安全的 OSSpinLock)
- 一般场景,直接用
@synchronized
。使用最方便。一般业务开发场景,锁的性能影响不大,能力上也只需要简单的互斥锁,因此怎么方便怎么来。而且@synchronized
性能也没有差太多。 - 性能苛刻的场景:
os_unfair_lock
,自旋锁废弃后官方推荐的替代品,性能优异。 - 需要信号量:
dispatch_semaphore
- 需要条件锁:
NSCondition
- 需要读写锁:
pthread_rwlock
- 需要递归锁:
NSRecursiveLock
使用
1. 自旋锁 OSSpinLock
自旋锁是这些锁中唯一一个依靠忙等待实现的锁,也就是说可以理解成一个暴力的 while 循环,因此会浪费较多的 CPU,但它是所有锁中性能最高的。适用于对时延要求比较苛刻、临界区计算量比较小、本身 CPU 不存在瓶颈的场景。
但是现在不能用了。YY 老师在不再安全的 OSSpinLock 中讲得很清楚了,当低优先级的线程已进入临界区,高优先级的线程想要获取资源就需要忙等待,占用大量 CPU,导致低优先级线程迟迟不能执行完临界区代码,导致类死锁的问题。
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
// do something
OSSpinLockUnlock(&lock);
2. os_unfair_lock
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock);
// do something
os_unfair_lock_unlock(&lock);
3. pthread_mutex
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
// do something
pthread_mutex_unlock(&lock);
4. dispatch_semaphore
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(lock);
dispatch_semaphore_create
传入参数是信号量的值,在这里就是能够同时进入临界区的线程数。dispatch_semaphore_wait
,当信号量大于 0 时减一并进入临界区,如果信号量等于 0 则等待直到信号量不为 0 或到达设定时间。dispatch_semaphore_signal
使信号量加 1。
5. NSLock
NSLock *lock = [NSLock new];
[lock lock];
// do something
[lock unlock];
6. NSCondition
条件锁,以生产者消费者模型为例
NSCondition *condition = [NSCondition new];
// Thread 1: 消费者
- (void)consumer
{[condition lock];
while(conditionNotSatisfied){[condition wait]
}
// 消费逻辑
consume();
[condition unlock];
}
// Thread 2: 生产者
- (void)producer
{[condition lock];
// 生产逻辑
produce();
[condition signal];
[condition unlock];
}
7. NSConditionLock
条件锁,跟 NSCondition 差不多,对条件做了封装,简化了使用但也没 NSCondition 那么灵活了。
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
// Thread 1: 消费者
- (void)consumer
{[condition lockWhenCondition:1];
while(conditionNotSatisfied){[condition wait]
}
// 消费逻辑
consume();
[condition unlockWithCondition:0];
}
// Thread 2: 生产者
- (void)producer
{[condition lock];
// 生产逻辑
produce();
[condition unlockWithCondition:1];
}
8. NSRecursiveLock
可以递归调用的互斥锁。
int i = 0;
NSRecursiveLock *lock = [NSRecursiveLock new];
- (void)testLock
{if(i > 0){[lock lock];
[self testLock];
i --;
[lock lock];
}
}
9. @synchronized
普通的锁,用着方便。
@synchronized(self) {// do something}
10. pthread_rwlock
读写锁,一般也不怎么用得上,这里给了个字典 set/get 的例子,但是实际业务场景,通常普通的互斥锁就可以了。
在读操作比写操作多很多的情况下,读写锁的收益比较可观。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
NSMutableDictionary *dic = [NSMutableDictionary new];
- (void)set
{
// 写模式加锁
pthread_rwlock_wrlock(&lock);
dic[@"key"] = @"value";
// 解锁
pthread_rwlock_unlock(&lock);
}
- (NSString *)get
{
NSString *value;
// 写模式加锁
pthread_rwlock_rdlock(&lock);
value = dic[@"key"];
// 解锁
pthread_rwlock_unlock(&lock);
return value;
}
推荐阅读
- 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么?– Tim Chen 的回答 – 知乎
- 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么?– 胖君的回答 – 知乎
- 不再安全的 OSSpinLock
- iOS 多线程安全 -13 种线程锁
- iOS 开发中的 11 种锁以及性能对比 )