由于线程共享了进程的资源空间,如果是多线程读写资源,就会出现同时对同一资源进行操作的情况,这样就会发生数据的读写错乱和不一致现象.为了保持数据的一致性,不至于出现多个线程同时修改相同资源的情况,我们需要为资源操作进行加锁处理,在一个线程访问资源时,对该资源进行加锁,确保在同一时间只有一个线程对资源进行操作,当线程对资源的操作结束之后,对该资源解除锁定,允许下一个线程对资源进行访问.
我们以景点的售票案例进行说明:假设由三个窗口同时进行开始销售100张车票,每出售一张,即将车票数量减去1,然后写如数据.如果不加锁在售票开始时,三个窗口获取到的车票数都是100,三个窗口同时各卖出一张车票,写会的数据均是99张,而事实上现在的车票只剩下97张,这就造成了数据的不一致:
static NSInteger total = 100; void (^sellTicket)(NSString *) = ^(NSString *name) { while (true) { if (total > 0) { total--; NSLog(@"%@:剩余%ld, current == %@", name, total, [NSThread currentThread]); } else { NSLog(@"车票已经售完"); break; } } }; if (@available(iOS 10.0, *)) { [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口01"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口02"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口03"); }]; }
然后你会发现,很多车票被多次出售了:
... 窗口01:剩余63, current == <NSThread: 0x6000014a4000>{number = 3, name = (null)} ... 窗口02:剩余63, current == <NSThread: 0x600001490e00>{number = 5, name = (null)} ...
锁是最常用的同步工具。一段代码段在同一个时间只能允许被有限个线程访问,比如一个线程 A 进入需要保护代码之前添加简单的互斥锁,另一个线程 B 就无法访问,只有等待前一个线程 A 执行完被保护的代码后解锁,B 线程才能访问被保护代码。iOS中常用的锁有十几中,我们仅以其中的几种进行说明
@synchronized [001]
同步锁,其中需要确保在加锁期间,参数不能为空,一般会选用线程所在的控制器:
static NSInteger total = 100; void (^sellTicket)(NSString *) = ^(NSString *name) { while (true) { [NSThread sleepForTimeInterval:arc4random() % 10 * 1.0 / 10]; @synchronized (self) { if (total > 0) { total--; NSLog(@"%@:剩余%ld, current == %@", name, total, [NSThread currentThread]); } else { NSLog(@"车票已经售完"); break; } } } }; if (@available(iOS 10.0, *)) { [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口01"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口02"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口03"); }]; }
仔细观察一下数据,发现每张票都只出售了一次.
NSLock [002]
@interface NSLock : NSObject <NSLocking> { @private void *_priv; } - (BOOL)tryLock; - (BOOL)lockBeforeDate:(NSDate *)limit; @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
NSLock 遵循 NSLocking 协议,lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
//主线程中 NSLock *lock = [[NSLock alloc] init]; //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程1:%@", [NSThread currentThread]); [lock lock]; NSLog(@"线程1"); [NSThread sleepForTimeInterval:2.0]; [lock unlock]; NSLog(@"线程1解锁"); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程2:%@", [NSThread currentThread]); [NSThread sleepForTimeInterval:1.0]; //使线程2在线程1之后执行 [lock lock]; NSLog(@"线程2"); [lock unlock]; });
输出结果:
线程1:<NSThread: 0x600000e2a440>{number = 3, name = (null)} 线程2:<NSThread: 0x600000e1d340>{number = 4, name = (null)} 线程1 线程1解锁 线程2
线程1中的lock加锁当前线程需要休眠2s,导致线程2中的加锁操作失败,线程2被阻塞.等到线程1休眠结束之后,进行解锁操作,这时线程2就可以进行加锁操作,从而进行后续的操作.从这个操作我们也可以看出来,
- lock操纵是会阻塞线程的;
- lock操作会轮询是否可以加锁,直到所在线程加锁成功,否则一直阻塞线程.所以如果操作需要加锁,不用判断是否可以加锁,直接调用lock方法对操作进行加锁.
然后尝试一下另一个操作:
//主线程中 NSLock *lock = [[NSLock alloc] init]; //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程1:%@", [NSThread currentThread]); [lock lock]; NSLog(@"线程1"); [NSThread sleepForTimeInterval:2.0]; [lock unlock]; NSLog(@"线程1解锁"); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程2:%@", [NSThread currentThread]); [NSThread sleepForTimeInterval:1.0]; if ([lock tryLock]) { [lock lock]; NSLog(@"线程2"); [lock unlock]; } else { NSLog(@"尝试加锁失败!"); } });
输出结果:
线程2:<NSThread: 0x600001f01e00>{number = 4, name = (null)} 线程1:<NSThread: 0x600001f3b540>{number = 3, name = (null)} 线程1 尝试加锁失败! 线程1解锁
由上面的结果可得知,tryLock 并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码.
再对上述代码做一下改动:
//主线程中 NSLock *lock = [[NSLock alloc] init]; //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程1:%@", [NSThread currentThread]); [lock lock]; NSLog(@"线程1"); [NSThread sleepForTimeInterval:2.0]; [lock unlock]; NSLog(@"线程1解锁"); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"线程2:%@", [NSThread currentThread]); CGFloat distance = 5.0f; //如果我们将distance = 0.5,那么该方法就会加锁失败,返回false. [NSThread sleepForTimeInterval:1.0]; if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:distance]]) { NSLog(@"线程2"); [lock unlock]; NSLog(@"线程2解锁"); } else { NSLog(@"%lfs之内尝试加锁失败", distance); } });
lockBeforeDate:方法会在指定的Date之前尝试进行加锁操作,会阻塞线程,如果可以加锁成功则会返回true;如果在指定时间Date之后未能完成加锁,则会返回false.
另外需要注意的是:
- lock(或者locklockBeforeDate:)必须与unlock方法成对出现,如果多次lock会造成死锁;
- 只有在unlock之后才能再次进行lock操作;
- 必须在同一线程中进行进行加锁解锁操作.
例如下面的操作如果使用NSLock就会造成死锁:
NSLock *lock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveBlock)(int); RecursiveBlock = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value:%d", value); RecursiveBlock(value - 1); } [lock unlock]; }; RecursiveBlock(2); });
这是因为第一次加锁之后,还未执行解锁就进入了递归的下一层,而再次请求加锁,该操作阻塞了当前线程,导致解锁的操作永远不被执行从而形成死锁.为了解决这一问题,iOS中出现了另一种锁---NSRecursiveLock.
NSRecursiveLock [003]
@interface NSRecursiveLock : NSObject <NSLocking> { @private void *_priv; } - (BOOL)tryLock; - (BOOL)lockBeforeDate:(NSDate *)limit; @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
NSRecursiveLock是递归锁,与NSLock不同的是,NSRecursiveLock允许在同一线程中重复加锁,NSRecursiveLock会记录加锁与解锁的次数,当两者平衡时才会释放锁,其他的线程才可以再对同一资源进行加锁.
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveBlock)(int); RecursiveBlock = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value:%d", value); RecursiveBlock(value - 1); } [lock unlock]; }; RecursiveBlock(2); });
输出结果:
value:2 value:1
NSConditionLock [004]
NSConditionLock 和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法.
@property (readonly) NSInteger condition; - (void)lockWhenCondition:(NSInteger)condition; - (BOOL)tryLock; - (BOOL)tryLockWhenCondition:(NSInteger)condition; - (void)unlockWithCondition:(NSInteger)condition; - (BOOL)lockBeforeDate:(NSDate *)limit; - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
NSConditionLock 可以称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 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的代码后执行 if ([lock tryLockWhenCondition:2]) { NSLog(@"线程4"); [lock unlockWithCondition:1]; NSLog(@"线程4解锁成功"); } else { NSLog(@"线程4尝试加锁失败"); } });
输出结果:
线程2 线程2解锁成功 线程3 程3解锁成功 线程4 线程4解锁成功 线程1
上述代码中,初始化的条件锁条件为condition=0,只有线程2满足condition=0的条件可以加锁.而线程1不满足加锁条件,会阻塞线程;线程3需要休眠2s之后执行,线程4需要在休眠3s之后执行,而且线程3,线程4也不满足加锁条件.当线程2执行解锁操作之后condition=2,这时线程3,线程4都满足条件,而且线程解锁之后condition=2,所以线程3早于线程4执行,当线程4执行结束以后,设置condition=1,满足线程1加锁条件,线程1不再堵塞线程开始加锁,执行线程中的后续操作. 从中也可以看出,合理地使用NSConditionLock条件锁可以实现任务之间的依赖.
NSCondition [005]
NSCondition是一个比较特殊的锁,它可以实现两个不同线程之间调度,它具有两个功能:锁定资源和线程检查器.当条件不满足时会阻塞当前线程等待另一线程发送信号使得条件满足时,才会激活线程继续执行操作.
@interface NSCondition : NSObject <NSLocking> { @private void *_priv; } - (void)wait; //挂起线程 - (BOOL)waitUntilDate:(NSDate *)limit; //什么时候挂起线程 - (void)signal; // 唤醒一条挂起线程 - (void)broadcast; //唤醒所有挂起线程 @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
例如我们可以利用这个特性来实现iOS的同步请求:
dispatch_queue_t queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_async(queue, ^{ NSCondition *lock = [[NSCondition alloc] init]; __block NSDictionary *_responseObject = nil; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [lock lock]; _responseObject = responseObject; [lock signal]; [lock unlock]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [lock lock]; _responseObject = @{}; [lock signal]; [lock unlock]; }]; [lock lock]; if (!_responseObject) { [lock wait]; } NSLog(@"responseObject == %@", _responseObject); [lock unlock]; });
dispatch_semaphore [006]
dispatch_semaphore 使用信号量机制实现锁,主要通过pv操作来实现同步和互斥.等待信号和发送信号.主要有三个函数:
dispatch_semaphore_create(long value); //创建信号量 dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout); //等待资源 dispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema); //发送信号量
- dispatch_semaphore_create:可以初始化信号量的数量,当信号量不为0时可以进行操作,信号量为0时进行等待;
- dispatch_semaphore_wait:如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回;
- dispatch_semaphore_signal:发送信号量。该函数会对信号量的值进行加1操作.
例如
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_semaphore_wait(semaphore, overTime); NSLog(@"线程1开始"); sleep(5); NSLog(@"线程1结束"); dispatch_semaphore_signal(semaphore); }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); dispatch_semaphore_wait(semaphore, overTime); NSLog(@"线程2开始"); dispatch_semaphore_signal(semaphore); });
输出结果:
线程1开始 线程1结束 线程2开始
初始化创建了一个信号量,进入线程1时,由于信号量不为0,所以线程1继续执行代码,同时信号量减1,此时信号量变为0.然后线程1休眠5s,在这期间线程2开始执行,由于此时信号量为0,所以线程2倍阻塞,等待信号量.5s之后线程1激活,执行任务,发送信号使信号量为1,此时线程2获得信号,激活线程开始执行任务,同时将信号量减变为0.线程2执行结束之后发送信号,使信号量重新变为1.
pthread_mutex [007]
pthread也是多线程的实现技术之一,而且具有跨平台的特,只不过使用起来比较复杂,且面向过程,在iOS 开发中并不常用.而pthread_mutex却是一种比较易用的互斥锁,使用步骤:
- 使用 pthread_mutex_init 初始化一个 pthread_mutex_t;
- pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定;
- pthread_mutex_unlock 来解锁;
- 当使用完成后调用 pthread_mutex_destroy 来销毁锁.
pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable);//初始化,普通锁后一个参数可以为NULL,递归锁需要设置指定的pthread_mutexattr_t pthread_mutex_lock(pthread_mutex_t * _Nonnull); //加锁,加锁成功返回0,否则返回错误码 pthread_mutex_trylock(pthread_mutex_t * _Nonnull);//加锁成功返回的是 0,失败返回的是错误提示码 pthread_mutex_unlock(pthread_mutex_t * _Nonnull); //解锁 pthread_mutex_destroy(pthread_mutex_t * _Nonnull); //销毁锁 int pthread_mutexattr_settype(pthread_mutexattr_t *, int);//递归锁时需要设置类型,默认类型PTHREAD_MUTEX_DEFAULT,不可以重复加锁
在使用时,如果需要重复加锁(在解锁之前重新加锁,即递归锁),则需要使用来设置pthread_mutexattr_settype类型,否则会造成死锁.
例如:
__block pthread_mutex_t lock; pthread_mutex_init(&lock, NULL); static NSInteger total = 100; void (^sellTicket)(NSString *) = ^(NSString *name) { while (true) { sleep(0.6f); pthread_mutex_lock(&lock); if (total > 0) { total--; NSLog(@"%@:剩余%ld, current == %@", name, total, [NSThread currentThread]); } else { NSLog(@"车票已经售完"); pthread_mutex_destroy(&lock); break; } pthread_mutex_unlock(&lock); } }; if (@available(iOS 10.0, *)) { [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口01"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口02"); }]; [NSThread detachNewThreadWithBlock:^{ sellTicket(@"窗口03"); }]; }
但是如果遇到了递归锁,这样马上就死翘翘了:
__block pthread_mutex_t lock; pthread_mutex_init(&lock, NULL); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveBlock)(int); RecursiveBlock = ^(int value) { pthread_mutex_lock(&lock); if (value > 0) { NSLog(@"value:%d", value); RecursiveBlock(value - 1); } else { pthread_mutex_destroy(&lock); } pthread_mutex_unlock(&lock); }; RecursiveBlock(2); });
输出结果 :
value:2
看得出来只输出了第一次加锁时的内容,所以线程被阻塞了.这时我们我们需要使用递归锁,而pthread_mutex_t是可以支持递归锁的:
__block pthread_mutex_t lock; pthread_mutexattr_t arr; pthread_mutexattr_settype(&arr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&lock, &arr); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveBlock)(int); RecursiveBlock = ^(int value) { pthread_mutex_lock(&lock); if (value > 0) { NSLog(@"value:%d", value); RecursiveBlock(value - 1); } else { pthread_mutex_destroy(&lock); } pthread_mutex_unlock(&lock); }; RecursiveBlock(2); });
输出结果:
value:2 value:1
OSSpinLock
在互斥锁中,如果锁被线程保持,其他尝试加锁的线程会被阻塞不再占用CPU资源,等待锁被释放之后再唤起其他线程进行加锁.而对于自旋锁来讲,如果自旋锁已经被别的线程保持,调用线程就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这样就会导致线程一直处于忙等状态,"自旋"一词就是因此而得名.所以自旋锁在执行单元保持锁的时间很短时,很有优势,不用频繁地休眠唤起线程从而拥有比较高的效率.
typedef int32_t OSSpinLock; // 加锁 void OSSpinLockLock( volatile OSSpinLock *__lock ); // 尝试加锁 bool OSSpinLockTry( volatile OSSpinLock *__lock ); // 解锁 void OSSpinLockUnlock( volatile OSSpinLock *__lock );
例如:
__block OSSpinLock theLock = OS_SPINLOCK_INIT; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OSSpinLockLock(&theLock); NSLog(@"线程1开始"); sleep(3); NSLog(@"线程1结束"); OSSpinLockUnlock(&theLock); }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OSSpinLockLock(&theLock); sleep(1); NSLog(@"线程2"); OSSpinLockUnlock(&theLock); });
由于自旋锁的效率会比互斥锁高,使用的频率非常高.尤其是ReactiveCocoa大量使用了这种锁.当然这种锁也并不是绝对安全的,YY大神 @ibireme 的文章 不再安全的 OSSpinLock 说这个自旋锁存在优先级反转问题,有兴趣的可以详细了解一下.
正式因为自旋锁存在线程安全问题,所以在iOS10.0之后的api中开始废弃自旋锁,使用os_unfair_lock来进行替换.
if (@available(iOS 10.0, *)) { __block os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; os_unfair_lock_lock(&lock); os_unfair_lock_unlock(&lock); [NSThread detachNewThreadWithBlock:^{ os_unfair_lock_lock(&lock); NSLog(@"线程1开始执行"); sleep(5.0f); NSLog(@"线程1执行结束"); os_unfair_lock_unlock(&lock); }]; [NSThread detachNewThreadWithBlock:^{ os_unfair_lock_lock(&lock); sleep(1.0); NSLog(@"线程2开始执行"); NSLog(@"线程2执行结束"); os_unfair_lock_unlock(&lock); }]; }
该api还有两个断言语法用作安全检查:
//断言当前线程是拥有lock的线程:如果是,则返回,否则会终止执行 void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock); //断言当前线程不是拥有锁的线程:如果不是,则返回,否则会终止执行 void os_unfair_lock_assert_owner(os_unfair_lock_t lock);