多线程模型下,由于共享内存带来的冲突风险,锁是个避不开的话题。
关于锁
首先从平台无关的角度看,从能力上区分,主要有以下几种锁:
- 互斥锁(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种锁以及性能对比 )