iOS-锁
一 、线程安全
1.1 什么是线程安全
线程操作共享数据的时候不会出现意想不到的结果就叫线程安全,否则,就是线程不安全
1.2 原子属性是一定是线程安全的?
原子属性只能保障 set 或者 get的读写安全,但我们在使用属性的时候,往往既有set又有get,所以说原子属性并不是线程安全的。
二、 iOS中的三种锁
2.1 自旋锁
在访问被锁的资源的时候,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。(忙等)。
优点:因为自旋锁不会引起调用者线程休眠,所以不会进行线程调度,cpu时间片轮转等一些耗时的操作。所以如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁
缺点:自旋锁一直占用CPU,在未获得锁的情况下,一直自旋,相当于死循环,会一直 占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。 而且自旋锁不能实现递归调用
存在的bug:(优先级反转):
当多个线程有优先级的时候,如果一个优先级低的线程先去访问某个数据,此时使用自旋锁进行了加锁,然后一个优先级高的线程又去访问这个数据,那么优先级高的线程因为优先级高会一直占着 CPU资源,此时优先级低的线程无法与优先级高的线程争夺 CPU 时间,从而导致任务迟迟无法完成、锁无法释放。由于自旋锁本身存在的这个问题,所以苹果在iOS10以后已经废弃了OSSpinLock
注意点:如果我们无法保证锁的线程全部处于同一个优先级,就不要使用自旋锁。
2.2 互斥锁(多线程编程中)(又分为递归锁和非递归锁)
互斥锁的出现解决了自旋锁优先级反转的问题。互斥锁是指在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁的资 源释放锁。然后再唤醒休眠线程。(闲等)
互斥锁又分为递归锁和非递归锁 1. 递归锁是可重复加锁,在锁释放之前可再次获取锁(NSRecursiveLock) 2. 非递归锁不能重复加锁,必须等锁释放后才能再次获取锁 (NSLock)
2.3 读写锁
1. 多读单写:在同一时刻可以被多条线程进行读取数据的操作,但是在同一时刻只能有一条线程 在写入数据。 2. 读写互斥:在同以时刻,读和写不能同时进行。
2.4 锁的运行效率对比
锁在运行的时候的效率也各不相同,以下是对比。其中@synchronized
在真机条件下效率会有所提升。在开发时,我们可以根据不同情况,选择适合的锁,
来保证我们的代码线程安全。
三、详细介绍iOS中的各种锁
3.1 案例
在下面的这段代码中因为开启了异步线程导致count并不能顺序的执行下去,也就是前面说提到的是线程不安全的,那么我们可以借助iOS中的各种锁来解决这个问题。
- (void)test { for (int i = 0; i < 10; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ //初始值为50 self.count --; NSLog(@"%d",self.count); }); } }
3.2 各种锁的初始化定义
@property (nonatomic ,assign) int count; @property (nonatomic ,assign) os_unfair_lock unfairLock; @property (nonatomic ,strong) NSLock *iLock; @property (nonatomic ,strong) NSCondition *iCondition; @property (nonatomic ,strong) NSConditionLock *iConditionLock; @property (nonatomic ,strong) NSRecursiveLock *iRecursiveLock; @property (nonatomic ,strong) dispatch_queue_t iQueue; @property (nonatomic ,strong) NSMutableDictionary *dataDic; self.count = 50; self.unfairLock = OS_UNFAIR_LOCK_INIT; self.iLock = [[NSLock alloc] init]; self.iCondition = [[NSCondition alloc] init]; self.iRecursiveLock = [[NSRecursiveLock alloc] init]; self.iQueue = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT); self.dataDic = [NSMutableDictionary new]; [self lg_write:@"LG"];
3.3 各种锁的结局方案
3.3.1 OSSpinLock
1. OSSpinLock 是自旋锁,会出现优先级反转的问题,在iOS10之后被弃用,需要引用头文件<libkern/OSAtomic.h> //一些基本操作 OS_SPINLOCK_INIT //初始化锁 OSSpinLockLock(&spinlock) //加锁,参数为OSSPINLOCK地址 OSSpinLockUnlock(&spinlock) //解锁,参数是OSSpinLock地址 OSSpinLockTry(&spinlock) //尝试上锁,参数是OSSpinLock地址。如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功
- (void)OSSpinLock_test { OSSpinLockLock(&_spinLock); self.count --; NSLog(@"%d",self.count); OSSpinLockUnlock(&_spinLock); }
3.3.2 os_unfair_lock
os_unfair_lock 是互斥锁,iOS10后开始支持,取代OSSpinLock //一些基本操作 OS_UNFAIR_LOCK_INIT //初始化锁 os_unfair_lock_lock //加锁。参数为os_unfair_lock地址 os_unfair_lock_unlock //解锁。参数为os_unfair_lock地址 os_unfair_lock_trylock //尝试加锁。参数为os_unfair_lock地址。如果成功返回true。如果锁已经被锁定则返回false
//这两个方法主要用来判断当前线程是否持有这个锁
os_unfair_lock_assert_owner //参数为os_unfair_lock地址。如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃 os_unfair_lock_assert_not_owner //参数为os_unfair_lock地址。如果当前线程持有指定的锁,则触发崩溃
//使用 -(void)unfairLock_test { os_unfair_lock_lock(&_unfairLock); self.count --; NSLog(@"%d",self.count); os_unfair_lock_unlock(&_unfairLock); }
3.3.3 NSLock
NSLock基于pthread封装,需要注意的一点是NSLock不能重复加锁解锁,否则会导致程序crash //一些基本操作 - (void)lock //加锁 - (void)unlock //解锁 - (BOOL)tryLock //尝试加锁。成功返回YES,失败返回NO - (BOOL)lockBeforeDete:(NSDate *)limit //在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO @property (nullable ,copy) NSString *name // 锁名称
-(void)nslock_test { [self.iLock lock]; self.count --; NSLog(@"%d",self.count); [self.iLock unlock]; }
3.3.4 NSCondition
NSCondition存在虚假唤醒的问题 //一些基本操作 - (void)lock //加锁 - (void)unlock //解锁 - (void)wait // 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁 - (void)waitUntilDate //阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁 - (void)signal //唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁 - (void)broadcast //唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁 @property (nullable ,copy) NSString *name //锁名称
NSCondition存在的虚假唤醒的问题:
当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满足时,就会发生虚假唤醒。
之所以称为虚假,是因为该线程似乎无缘无故地被唤醒了。
原因:signal唤醒时系统将其理解为了broadcast
在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变量发出信号时等待它,
系统可能会决定将它们全部唤醒,将每个signal( )唤醒一个线程视为 broadcast( )唤醒所有这些,
从而打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线程在等待,那么只有一个会获胜,另外9个会经历虚假唤醒。
//例子
- (void)nscondition_test { for (int i = 0; i < 50; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self lg_production]; }); } for (int i = 0; i < 100; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self lg_consumption]; }); } } - (void)lg_production { [self.iCondition lock]; self.count ++; NSLog(@"生产了一个产品,现有产品 : %d个",self.count); [self.iCondition signal]; [self.iCondition unlock]; } - (void)lg_consumption { //虚假唤醒 [self.iCondition lock]; while (self.count == 0) {//if是虚假唤醒,可以改为while就不会虚假唤醒 [self.iCondition wait]; } self.count --; NSLog(@"消费了一个产品,现有产品: %d个",self.count); [self.iCondition unlock]; }
3.3.5 NSConditionLock
NSConditionLock是基于NSCondition的封装 //一些基本操作 - (void)lock //加锁 - (void)unlock //解锁 - (instancetype)initWithCondition:(NSinteger) //初始化一个。NSConditionLock对象(设置了初始值) @property(readonly) NSInteger condition //锁的条件 - (void)lockWhenCondition:(NSInteger)conditio //满足条件时加锁 - (BOOL)tryLock //尝试加锁 - (BOOL)tryLockWhenCondition //如果接受对象的condition与给定的condition相等,则尝试获取锁,不足塞线程 - (void)unlockWithCondition:(NSInteger)condition //解锁,重置锁的条件 - (BOOL)lockBeforDate:(NSDate *) //limit在指定时间点之前获取锁 - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *) //limit在指定的时间前获取锁 @property (nullable ,copy) NSString *name //锁名称
//例子 - (void)lg_testConditonLock{ //如何让123变的顺序 //第一种方法:使用信号量 //第二种方法:使用NSConditonLock self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ //只有与初始化值相同的时候锁才能初始化 [self.iConditionLock lockWhenCondition:3]; NSLog(@"线程 1"); [self.iConditionLock unlockWithCondition:2]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.iConditionLock lockWhenCondition:2]; NSLog(@"线程 2"); [self.iConditionLock unlockWithCondition:1]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.iConditionLock lockWhenCondition:1]; NSLog(@"线程 3"); [self.iConditionLock unlockWithCondition:0]; }); }
3.3.6 NSRecursiveLock
NSRecursiveLock是递归锁,在递归函数的时候使用 //一些操作 - (void)lock //加锁 - (void)unlock //解锁 - (BOOL)tryLock //尝试加锁。成功返回YES,失败返回NO - (BOOL)lockBeforeDete:(NSDate *)limit //在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO @property (nullable ,copy) NSString *name //锁名称。
-(void)recursiveLock_test { [self.iRecursiveLock lock]; self.count --; NSLog(@"%d",self.count); [self.iRecursiveLock unlock]; }
//注意点 递归锁不可以在多个线程下递归调用 - (void)recursiveTest { //如果这里加for循环async时 //因为这个锁是递归锁,他可以在同一时刻能够被多个线程所拥有 //但解锁的时候需要保证该锁不被其他线程所拥有,那么就会导致多个线程此时就会产生相互等待彼此解锁的死锁情况 //所以递归锁不能在多个线程下递归调用 此时就可以用@synchronized dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^recursiveMethod)(int); recursiveMethod = ^(int value){ if (value > 0) { //@synchronized(self) [self.iRecursiveLock lock]; NSLog(@"%d",value); recursiveMethod(value - 1); [self.iRecursiveLock unlock]; } }; recursiveMethod(10); }); }
3.3.7 @synchronized
@synchronized是非常简单的一把锁 //使用 - (void)synchronizedTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^recursiveMethod)(int); recursiveMethod = ^(int value){ if (value > 0) { @synchronized(self) NSLog(@"%d",value); recursiveMethod(value - 1); } }; recursiveMethod(10); }); }
3.3.8 信号量
信号量锁 //一些基本操作 dispatch_semaphore_create(intptr_t value) //创建信号量,并且创建的时候需要指定信号量的大小 dispatch_semaphore_wait(dispatch_semaphore_t dsema, diapatch_time_t timeout) //等待信号量,如果信号量等于0,那么该函数就会一直等待(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回 dispatch_semaphore_signal(dispatch_semaphore_t dsema) //发送信号量,该函数会对信号量的值进行加1操作
//使用 - (void)lg_dispatch_semaphore_t { dispatch_semaphore_t sem = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务1"); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务2"); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务3"); }); }
3.3.9 pthread_mutex
pthread_mutex是C语言实现的锁,需要开发人员自己实现锁生命周期的管理 //一些基本操作 pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr) //初始化锁,pthread_mutexattr_t可用来设置锁的类型 pthread_mutex_lock(pthread_mutex_t mutex) //加锁 pthread_mutex_trylock(*pthread_mutex_t *mutex) //加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息 pthread_mutex_unlock(pthread_mutex_t *mutex) //释放锁 pthread_mutex_destroy(pthread_mutex_t* mutex) //使用完锁之后释放锁 pthread_mutexattr_setpshared() //设置互斥锁的范围 pthread_mutexattr_getpshared() //获取互斥锁的范围
//使用 - (void)lg_pthread_mutex { //非递归 pthread_mutex_t lock0; pthread_mutex_init(&lock0, NULL); pthread_mutex_lock(&lock0); pthread_mutex_unlock(&lock0); pthread_mutex_destroy(&lock0); //递归 pthread_mutex_t lock; pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&lock, &attr); pthread_mutexattr_destroy(&attr); pthread_mutex_lock(&lock); pthread_mutex_unlock(&lock); pthread_mutex_destroy(&lock); }
3.3.10 读写锁(栅栏函数+异步函数实现)
//多读单写,读写互斥 @property (nonatomic ,strong) dispatch_queue_t iQueue; self.iQueue = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT); - (NSString *)lg_read { // 异步读取 __block NSString *ret; dispatch_sync(self.iQueue, ^{ // 读取的代码 ret = self.dataDic[@"name"]; }); NSLog(@"%@",ret); return ret; } -(void)multiRead{ // 多读可以用for循环 for(int i = 0 ; i < 10 ;i++){ dispatch_async(self.iQueue, ^{ // 读取的代码 [self lg_read]; }); } } - (void)lg_write: (NSString *)name { // 写操作 //栅栏函数保证了读写互斥 dispatch_barrier_async(self.iQueue, ^{ [self.dataDic setObject:name forKey:@"name"]; }); }