概述
iOS多线程开发,会出现数据竞争,因此需要锁来保证线程安全。
线程安全
当一个线程访问资源时,需要保证其它的线程不能访问同一资源,即同一时刻只能有一个线程对数据进行操作。因此需要锁来保证线程的安全。
锁的使用步骤
- 准备一把锁:递归锁、互斥锁、条件锁、信号量等等
- 线程1中:
- 加锁(
- (void)lock;
) - 处理数据
- 解锁(
- (void)unlock;
)
- 加锁(
- 线程2中:
- 等待锁在线程1中使用完毕,并解锁
- 加锁(
- (void)lock;
) - 处理数据
- 解锁(
- (void)unlock;
)
从原理上讲锁分为哪几种?
-
自旋锁(叫循环判断锁更恰当一些)
已加锁时一直循环处于判断 lock 状态,如果 lock = 0 接着循环,如果 lock = 1 跳出循环转去临界区。因此,如果别的线程长期持有该锁,那么这个线程就一直在 while 地检查是否能够加锁,浪费 CPU 做无用功;如果拥有锁后很快释放锁,那么自旋锁就比较高效。一般用于多核 CPU
while 抢锁(lock) == 没抢到 { }
-
互斥锁
已加锁时阻塞掉当前线程,让出 CPU 资源,通过减少 CPU 的浪费来提高效率。操作系统负责线程调度,为了实现“锁的状态发生改变时再唤醒”,就需要把锁也交给操作系统管理。所以这个过程需要进行上下文的切换,保存寄存器状态需要花费时间,操作花销较自旋锁更大
while 抢锁(lock) == 没抢到 { 本线程先去睡了,请在这把锁的状态发生改变时再唤醒 }
-
读写锁(共享-独占锁)
是一种读共享,写独占的锁。主要有两种特性:
- 当读写锁被加了写锁时,其他线程对该锁加度锁或者写锁都会阻塞(不是失败)
- 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功
iOS 开发中常用的几种锁分别属于哪种类型?
自旋锁
os_unfair_lock
OSSPinLock:iOS 10.0 以后被弃用
互斥锁
- pthread_mutex:有
PTHREAD_MUTEX_NORMAL
、PTHREAD_MUTEX_ERRORCHECK
、PTHREAD_MUTEX_RECURSIVE
三种类型 - NSLock:对 mutex 普通锁的封装
- NSRecursiveLock:对 mutex 递归锁的封装
- NSCondition:对 mutex 和 条件的封装
- NSConditionLock: 对 NSCondition 的进一步封装,可以设置具体的条件
- 信号量 dispatch_semaphore:如果信号量不小于 0,立刻返回,否则是线程睡眠,让出时间片,导致操作系统切换到另一个线程,会花费 10us左右时间,并且需要切换两次
- @synchronized:是对 mutex 递归锁的封装,
@sychronized(obj)
内部会生成 obj 对应的递归锁,然后进行加锁、解锁操作 - atomic:用于保证属性的 setter、getter 的原子属性操作,相当于在 getter 和 setter 内部加了线程同步的锁。它不能保证使用属性的过程是线程安全的
读写锁
- pthread_rwlock:需要注意以下场景
- 同一时间,只能有 1 个线程进行写操作
- 同一时间,允许有多个线程进行多操作
- 同一时间,不允许既有写操作,又有读操作
常用的锁
开发中常用如下几种锁
NSRecursiveLock:递归锁
NSLock
多次 lock
却没有unlock
会导致死锁,例如下列情形,在递归调用中,会出现死锁。
self.myLock = [NSLock new];
dispatch_async(self.queue_1, ^{
static void (^recursiveBlock)(int);
recursiveBlock = ^(int value) {
[self.myLock lock];
if (value > 0) {
recursiveBlock(value - 1);
}
[self.myLock unlock];
};
recursiveBlock(10);
});
因此有了递归锁(NSRecursiveLock
),将上述代码中的NSLock
换成NSRecursiveLock
即可解决问题。递归锁其他用法与NSLock
一致。如下代码所示
dispatch_async(self.queue_1, ^{
static void (^recursiveBlock)(int);
recursiveBlock = ^(int value) {
[self.recursiveLock lock];
if (value > 0) {
recursiveBlock(value - 1);
}
[self.recursiveLock unlock];
};
recursiveBlock(10);
});
dispatch_async(self.queue_2, ^{
BOOL x = [self.recursiveLock lockBeforeDate:[NSDate distantFuture]];
if (x) {
[self.recursiveLock unlock];
} else {
NSLog(@"线程__2:__获取__锁__失败");
}
});
NSConditionLock:条件锁
NSConditionLock
相比于NSLock
多了一个条件
当两个线程需要根据特定的条件或者按照特定的顺序执行时,就需要NSConditionLock
。例如代码中开启了线程一下载图片,线程二处理图片。只有线程一下载图片完成后,线程二才能开始处理图片。在线程一下载完成之前,线程二处于阻塞状态。
self.conditionLock = [[NSConditionLock alloc] initWithCondition:kConditionOne];
dispatch_async(self.queue_1, ^{
[self.conditionLock lockWhenCondition:kConditionOne];
sleep(5);
[self.conditionLock unlockWithCondition:kConditionTwo];
});
dispatch_async(self.queue_2, ^{
[self.conditionLock lockWhenCondition:kConditionTwo];
sleep(5);
[self.conditionLock unlock];
});
- 条件锁在初始化的时候,设定了一个条件
kConditionOne
-
[self.conditionLock unlockWithCondition:kConditionTwo];
解锁时重新给NSConditionLock
设定了一个条件为kConditionTwo
- 当满足条件
kConditionTwo
时,可以重新获取这把锁 - 最后不需要更改获取锁的条件了,直接解锁
NSCondition
NSCondition
是一种特殊的锁,与NSConditionLock
类似,但是实现方式不一样。
dispatch_async(self.queue_1, ^{
NSLog(@"线程__1:__准备获取__锁__");
[self.myCondition lock];
NSLog(@"线程__1:__获取__锁__成功,并开始等待");
[self.myCondition wait];
NSLog(@"线程__1:__结束等待");
[self.myCondition unlock];
NSLog(@"线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
NSLog(@"线程__2:__准备获取__锁__");
[self.myCondition lock];
NSLog(@"线程__2:__获取__锁__成功,并开始等待");
[self.myCondition signal];
NSLog(@"线程__2:__发出信号");
[self.myCondition unlock];
NSLog(@"线程__2:__解__锁__成功");
});
上述代码运行结果
其中
-
- (void)wait;
会阻塞当前线程 -
- (void)signal;
激活一个阻塞的线程 -
- (void)broadcast;
激活所有阻塞的线程
备注:- 通过测试发现,
- (void)signal;
按照调用- (void)wait;
方法先后顺序激活线程,并不是首先激活与调用- (void)signal;
方法的线程 - 可以多次调用
- (void)signal;
方法依次激活被- (void)wait;
阻塞的线程
- 通过测试发现,
dispatch_semaphore:信号量
信号量类似于自动取款机。一次只能有一个人使用取款机。如果来的人多了,只能在旁边等着,如果使用取款机的人办完业务了,下一个人才能继续使用。
-
dispatch_semaphore_create(1)
传入值需>=0,若传入0,则阻塞线程 -
dispatch_semaphore_wait(semaphore, timeout);
,等待timeout,到了时间后会继续执行代码;或者信号量semaphore大于0也会继续执行代码 -
dispatch_semaphore_signal(signal);
类似于unlock,信号量会+1
信号量原理:首先把信号量减一,如果不小于零,就立刻返回,否则就使线程睡眠,让出时间片。主动让出时间片会导致操作系统切换到另一个线程,会花费10us左右的时间,并且需要切换两次,因此如果忙等时间只有几微秒,忙等比线程睡眠更高效。
如下述代码所示。
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_queue_t queue1 = dispatch_queue_create("globalQueue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
NSLog(@"线程1:等待");
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog(@"线程1:启动");
dispatch_semaphore_signal(signal);
NSLog(@"线程1:新的信号");
});
实例:有三个任务异步A、B、C,其中需要在A、B执行完毕后才可以执行任务C
可以使用信号量解决:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"任务A执行");
dispatch_semaphore_signal(semphore);
});
dispatch_async(queue, ^{
sleep(3);
NSLog(@"任务B执行");
dispatch_semaphore_signal(semphore);
});
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER); dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"C执行等待");
POSIX互斥锁
POSIX互斥锁是Linux/Unix平台上提供的API,C语言级别的锁,使用POSIX互斥锁需要引入头文件#import
,并申明初始化一个pthread_mutex_t
的结构。使用完毕,需要在- (void)dealloc;
中释放锁。
-
pthread_mutex_lock
加锁 -
pthread_mutex_unlock
解锁 -
pthread_mutex_destroy
释放锁
原理:pthread_mutex与信号量原理类似,不使用忙等,而是阻塞线程并睡眠,需要上下文切换,有多种类型,可通过定义锁的属性PTHREAD_MUTEX_NORMAL
、PTHREAD_MUTEX_ERRORCHECK
、PTHREAD_MUTEX_RECURSIVE
确定类型。
互斥锁内部会首先判断锁的类型,所以效率相对于信号量会低一些。
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr); // 创建锁
示例如下
dispatch_async(self.queue_1, ^{
NSLog(@"线程__1:__准备获取__锁__");
pthread_mutex_lock(&_mutex);
NSLog(@"线程__1:__获取__锁__成功");
sleep(5);
pthread_mutex_unlock(&_mutex);
NSLog(@"线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
NSLog(@"线程__2:__准备获取__锁__");
pthread_mutex_lock(&_mutex);
NSLog(@"线程__2:__获取__锁__成功");
pthread_mutex_unlock(&_mutex);
NSLog(@"线程__2:__解__锁__成功");
});
pthread_mutex_destroy(&_mutex);
结果如下
备注:POSIX提供了一整套完整的API,功能强大,非常底层。
NSLock:最基本的锁
- (void)lock;
- (void)unlock;
-
- (BOOL)lockBeforeDate:(NSDate *)limit;
在limit时间内尝试获取锁,为获取锁前一直阻塞线程,例如10s,如果10s内的时间,其它线程释放了锁(unlock),该方法会立刻获取锁
原理:NSLock
内部封装了属性为PTHREAD_MUTEX_ERRORCHECK
的pthread_mutex
,由于存在方法调用,因此会比pthread_mutex
更慢
注意:-
lock
和unlock
方法必须在同一线程中执行 - 连续
lock
中间没有unlock
会引起死锁
-
dispatch_async(self.queue_1, ^{
NSLog(@"线程1:等待");
[self.myLock lock];
NSLog(@"线程1");
sleep(5);
[self.myLock unlock];
NSLog(@"线程1:解锁成功");
});
dispatch_async(self.queue_2, ^{
NSLog(@"线程2:等待");
BOOL x = [self.myLock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:6]];
//BOOL x = [self.myLock lockBeforeDate:[NSDate distantFuture]];
if (x) {
NSLog(@"线程2:成功");
[self.myLock unlock];
} else {
NSLog(@"线程2:失败");
}
});
@synchronized
具体可以参考关于@synchronized 比你想知道的还多。@synchronized(objc)
会在运行时为 objc 分配递归锁,如果objc 为nil,则代码失去了线程安全性,如果在 @synchronized(objc)
中 objc 惜构,也不会产生问题
具体代码如下
dispatch_async(self.queue_1, ^{
NSLog(@"synch__线程__1:__准备获取__锁__");
@synchronized(self) {
NSLog(@"synch__线程__1:__获取__锁__成功");
sleep(5);
}
NSLog(@"synch__线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
NSLog(@"synch__线程__2:__准备获取__锁__");
@synchronized(self) {
NSLog(@"synch__线程__2:__获取__锁__成功");
}
NSLog(@"synch__线程__2:__解__锁__成功");
});
结果:
OSSpinLock:自旋锁
由于OSSpinLock存在优先级反转问题,在iOS 10.0被os_unfair_lock代替了
os_unfair_lock
在iOS 10.0后可用,用于代替自旋锁。使用时需要引入头文件#import
static os_unfair_lock unfairLock;
unfairLock = OS_UNFAIR_LOCK_INIT;
dispatch_async(self.queue_1, ^{
NSLog(@"unfairLock__线程__1:__准备获取__锁__");
os_unfair_lock_lock(&unfairLock);
NSLog(@"unfairLock__线程__1:__获取__锁__成功");
sleep(5);
os_unfair_lock_unlock(&unfairLock);
NSLog(@"unfairLock__线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
NSLog(@"unfairLock__线程__2:__准备获取__锁__");
os_unfair_lock_lock(&unfairLock);
NSLog(@"unfairLock__线程__2:__获取__锁__成功");
os_unfair_lock_unlock(&unfairLock);
NSLog(@"unfairLock__线程__2:__解__锁__成功");
});
性能分析
,性能如下:
-
测试环境:iPhone SE,iOS 10.0.1
-
测试环境:iPhone 7, iOS 11.4
上述图表是在单线程下测试的,并且只测试了加锁、解锁的性能,因此只能做定性分析。
从图表中可以看出,dispatch_semaphore性能最好,p thread_mutex、os_unfair_lock、NSCondition性能相近,pthread_mutex_recursive、NSRecursiveLock、NSLock性能次之,然后是NSConditionLock,synchronized性能最差。
未完待续
参考
- iOS 开发中的八种锁(Lock)
- 深入理解 iOS 开发中的锁
- iOS多线程-各种线程锁的简单介绍
- iOS多线程安全-13种线程锁