Created By Kunming
研究背景
我们在开发过程中,为了使应用更加高效、快速地运行,往往我们会使用到多线程来处理复杂的耗时逻辑。但使用了多线程后我们就常会遇到几个线程都要读写同一个资源的情况,若不对共享的资源进行同步限制的话,共享资源就会出现数据错乱的问题。线程锁就是用来处理上述问题的工具,因此掌握各个线程锁的用法及性能差异,在合适的场景下选择好合适的性能锁就显得尤为重要了。
iOS开发中常用的锁
- @synchronized 关键字加锁
- NSLock 对象锁
- NSRecursiveLock 递归锁
- NSConditionLock 条件锁
- pthread_mutex 互斥锁
- dispatch_semaphore 信号量实现加锁
- OSSpinLock
以下我们会逐一通过示例讲解各个所的用法以及各自的特点
@synchronized 关键字加锁
关键字加锁是iOS下的使用最简单的互斥锁,但是也是性能最差的一个。在通常情况下我们不建议使用。
注:什么是互斥锁?
如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。这种处理方式就叫做互斥锁。
使用方法:
@synchronized(这里添加一个OC对象,一般使用self) {
这里写要加锁的代码
}
注意点:
- 加锁的代码尽量少
- 添加的OC对象必须在多个线程中都是同一对象
- 优点是不需要显式的创建锁对象,便可以实现锁的机制。
- @synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
NSLock 对象锁
对象锁属于互斥锁,它使用也比较方便。但是注意不能多次调用lock的方法,否则会造成死锁的问题。
使用方法:
//主线程中
NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程1");
sleep(2);
[lock unlock];
NSLog(@"线程1解锁成功");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
NSLog(@"线程2");
[lock unlock];
});
注意点:
- NSLock的接口实际上都是通过NSLocking协议定义的,它定义了
lock
和unlock
方法。你使用这些方法来获取和释放该锁。 - 由于NSLock不能多次调用
lock
的方法,因此除了NSLocking协议提供的方法外,还提供了tryLock
和lockBeforeDate
两个方法。
tryLock试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。
因此NSLock
遇到递归等会重复调用lock
的方法时,采用tryLock
和lockBeforeDate
时只能避免线程死锁的问题。但NSLock
因为没有对实质问题进行处理因此在递归的情况下,使用对象锁仍然是不安全的。
NSRecursiveLock 递归锁
因为研究对象锁时我们指出过NSLock
最大的问题是处理递归问题时由于重复调用lock
方法导致死锁,递归锁就是专门为了处理递归流程设计的。与对象锁一样递归锁遵循NSLocking协议,并实现了tryLock
和lockBeforeDate
两个方法。
使用方法:
//创建锁
_rsLock = [[NSRecursiveLock alloc] init];
//线程1
dispatch_async(self.concurrentQueue, ^{
static void(^TestMethod)(int);
// 递归block
TestMethod = ^(int value)
{
[_rsLock lock];
if (value > 0)
{
[NSThread sleepForTimeInterval:1];
TestMethod(value--);
}
[_rsLock unlock];
};
TestMethod(5);
});
注意点:
- NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。
- 递归锁会跟踪它被多少次lock。每次成功的lock都必须平衡调用unlock操作。
- 只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。
NSConditionLock条件锁
顾名思义NSConditionLock
实现的需求是根据特定的条件对数据进行上锁和解锁。除了遵循NSLocking协议,并实现了tryLock
和lockBeforeDate
两个方法外,条件锁还提供- (void)lockWhenCondition:(NSInteger)condition
和 - (void)unlockWithCondition:(NSInteger)condition;
方法进行条件解锁。
使用方法:
//主线程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
}
else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// sleep(2);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock];
NSLog(@"线程3解锁成功");
}
else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// sleep(3);//以保证让线程2的代码后执行
[lock lockWhenCondition:2];
NSLog(@"线程4");
[lock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
});
pthread_mutex 互斥锁
第一步:初始化锁属性;第二步:初始化互斥锁,销毁锁属性;第三步:加锁 解锁;第四步:销毁互斥锁
使用方法:
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//线程1
dispatch_async(self.concurrentQueue), ^{
pthread_mutex_lock(&mutex);
NSLog(@"任务1");
sleep(2);
pthread_mutex_unlock(&mutex);
});
//线程2
dispatch_async(self.concurrentQueue), ^{
sleep(1);
pthread_mutex_lock(&mutex);
NSLog(@"任务2");
pthread_mutex_unlock(&mutex);
});
注意点:
-
申明一个互斥锁,pthread_mutex_t pMutex;
-
初始化它,pthread_mutex_init(&pMutex,NULL);
-
使用pMutex之前一定要初始化,否则不生效
-
获得锁,pthread_mutex_lock(&pMutex);
-
解锁,pthread_mutex_unlock(&pMutex);
dispatch_semaphore 信号量实现加锁
-
dispatch_semaphore_t dispatch_semaphore_create(long value);
这个方法就是利用给定的value值创建一个计数信号
信号值必须是>=0的整数
当不再使用信号时, 必须调用dispatch_release来释放semaphore对象 -
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个方法首先将semaphore对象的信号值-1, 当semaphore对象的信号值<0时, 当前线程锁住(处于休眠状态), 休眠的时间由timeout参数决定.
DISPATCH_TIME_FOREVER这个宏表示无限期休眠. -
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
这个方法首先将semaphore对象的信号值+1, 当semaphore对象的信号值>=0时, 当前线程被唤醒, 得以继续执行队列中的其他任务.
使用方法:
#import "NSLockTest.h"
#import <pthread.h>
@interface NSLockTest()
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
@end
@implementation NSLockTest
- (void)forTest
{
// 创建信号量
self.semaphore = dispatch_semaphore_create(2);
NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(download1) object:nil];
[thread1 start];
NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(download2) object:nil];
[thread2 start];
NSThread *thread3 = [[NSThread alloc]initWithTarget:self selector:@selector(download3) object:nil];
[thread3 start];
NSThread *thread4 = [[NSThread alloc]initWithTarget:self selector:@selector(download4) object:nil];
[thread4 start];
}
-(void)download1
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"download1 begin");
sleep(arc4random()%2);
NSLog(@"download1 end");
dispatch_semaphore_signal(self.semaphore);
[self download1];
}
-(void)download2
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"download2 begin");
sleep(arc4random()%2);
NSLog(@"download2 end");
dispatch_semaphore_signal(self.semaphore);
[self download2];
}
-(void)download3
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"download3 begin");
sleep(arc4random()%2);
NSLog(@"download3 end");
dispatch_semaphore_signal(self.semaphore);
[self download3];
}
-(void)download4
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"download4 begin");
sleep(arc4random()%2);
NSLog(@"download4 end");
dispatch_semaphore_signal(self.semaphore);
[self download4];
}
@end
注意点:
- dispatch_semaphore_t,使用过信号量来控制线程之间的同步
- 创建计数信号的时候传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
OSSpinLock 不再安全
OSSpinLock 已经不建议使用了,因为经过大神验证OSSpinLock已经不再可靠。
主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
为什么忙等会导致低优先级线程拿不到时间片?这还得从操作系统的线程调度说起。
现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。上面说到由于高优先级线程占用了大量时间片,没有再分配到时间片的低优先级线程就无法释放锁。
iOS开发中自旋和互斥锁的区别
相同点:
都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。
不同点:
互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
自旋锁的效率高于互斥锁。
使用自旋锁时要注意:
由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。
两种锁的加锁原理:
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换(主动出让时间片,线程休眠,等待下一次唤醒),cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环(忙等 do-while)检测锁的标志位,机制不复杂。
锁的选型
OSSPinLock,dispatch_semaphore的性能远远优于其他的锁,但是OSSpinLock由于优先级反转的问题,苹果在iOS 10的时候推出了os_unfair_lock来替代,而且性能不减当年,但是要在iOS 10之后才能用(虽然自旋锁的性能优于互斥锁),可以看出@synchronize和NSConditionLock明显性能最差,如果项目中对性能特别敏感,建议使用dispatch_semaphore,如果基于方便的话就用@synchronize就可以了
网友评论