本文名为《GCD 实现同步锁》,内容不止于锁。文章试图经过 GCD 同步锁的问题,尽可能往外延伸扩展,以讲解更多 GCD 同步机制的内容。 html
若是一段代码所在的进程中有多个线程在同时运行,那么这些线程就有可能会同时运行这段代码。假如多个线程每次运行结果和单线程运行的结果是同样的,并且其余的变量的值也和预期的是同样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来讲是原子操做或者多个线程之间的切换不会致使该接口的执行结果存在二义性,也就是说咱们不用考虑同步的问题。 linux
因为可读写的全局变量及静态变量能够在不一样线程修改,因此这二者也一般是引发线程安全问题的所在。在 Objective-C 中还包括属性和实例变量(实际上属性和实例变量本质上也能够看作类内的全局变量)。 编程
在 Objective-C 中,若是有多个线程执行同一份代码,那么有可能会出现线程安全问题。这种状况下,就须要一个同步机制来解决 —— 锁(lock)。在 Objective-C 中,有以下几种可用的锁: 安全
- NSLock 实现锁
NSLock是Cocoa提供给咱们最基本的锁对象,这也是咱们常常所使用的锁之一。
.- @synchronized 关键字构建的锁
synchronized指令实现锁的优势就是咱们不须要在代码中显式的建立锁对象,即可以实现锁的机制,但做为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。因此若是不想让隐式的异常处理例程带来额外的开销,你能够考虑使用锁对象。
.- 使用 C 语言的 pthread_mutex_t 实现的锁
.- 使用 GCD 来实现的“锁”
在GCD中也已经提供了一种信号机制,使用它咱们也能够来构建一把“锁”。从本质意义上讲,信号量与锁是有区别,具体差别参加信号量与互斥锁之间的区别。
.- NSRecursiveLock 递归锁
递归锁会跟踪它被多少次lock。每次成功的lock都必须平衡调用unlock操做。只有全部的锁住和解锁操做都平衡的时候,锁才真正被释放给其余线程得到。
.- NSConditionLock 条件锁
当咱们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能彻底知足咱们的使用。由于普通的锁只能关心锁与不锁,而不在意用什么钥匙才能开锁,而咱们在处理资源共享的时候,多数状况是只有知足必定条件的状况下才能打开这把锁。
.- NSDistributedLock 分布式锁
从它的类名就知道这是一个分布式的 Lock。NSDistributedLock 的实现是经过文件系统的,因此使用它才能够有效的实现不一样进程之间的互斥,但 NSDistributedLock 并不是继承于 NSLock,它没有 lock 方法,它只实现了 tryLock,unlock,breakLock,因此若是须要 lock 的话,你就必须本身实现一个 tryLock 的轮询。
补充:简单查了下资料,这个锁主要用于 OS X 的开发。而iOS 较少用到多进程,因此不多在 iOS 上见到过。因为精力有限,查询不够充分,若有错误请指出,谢谢!
在 GCD 以前,解决线程安全一般有两种锁。一是采用内置的同步锁 多线程
- (void)synchronizedMethod { @synchronized(self) { // safe code } }
这种写法会根据给定对象,自动建立一个锁,并等待块中的代码执行完毕,才释放锁。这段代码自己没什么问题,可是由于 @synchronized(self) 锁的对象是 self,形成共用此锁的同步块阻塞,下降效率。 并发
// someString 属性 // 当 someString 开始读时,对其的写入阻塞,这是合理的; - (NSString *)someString { @synchronized(self) { return _someString; } } - (NSString *)setSomeString:(NSString *)someString { @synchronized(self) { _someString = someString; } } //otherString 属性 // 当线程在对 someString 进行读写时,与之无关的 otherString 也会受到干扰阻塞,这是不合理的; - (NSString *)otherString { @synchronized(self) { return _otherString; } } - (NSString *)setOtherString:(NSString *)otherString { @synchronized(self) { _otherString = otherString; } }
此例子只用于说明 @synchronized(self) 的问题。聪明的同窗应该还会想到直接使用 atomic 来修饰属性,进行同步操做更简单直接。 异步
另外一种方法是使用 NSLock 对象 async
_lock = [[NSLock alloc] init]; - (void)synchronizedMethod { [_lock lock]; //safe code... [_lock unlock]; }
然而 NSLock 有可能在不经意间就形成了死锁 分布式
//主线程中 NSLock *theLock = [[NSLock alloc] init]; TestObject *aObject = [[TestObject alloc] init]; //线程1 //线程1 在递归的block内,可能会进行屡次的lock,而最后只有一次unlock dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void(^TestMethod)(int); TestMethod = ^(int value) { [theLock lock]; if (value > 0) { [aObject method1]; sleep(5); TestMethod(value-1); } [theLock unlock]; }; TestMethod(5); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); [theLock lock]; [aObject method2]; [theLock unlock]; });
这段代码就是一种典型的死锁状况,能够用递归锁 NSRecursiveLock 来避免这种状况。使用 NSRecursiveLock 类定义的锁会跟踪它被多少次 lock,每次成功的 lock 都必须平衡调用 unlock 操做。只有全部的上锁和解锁操做都平衡的时候,锁才真正被释放给其余线程得到。 函数
在讲解 GCD 同步机制前,先讲点 GCD 的基础知识。GCD 是异步任务的技术之一,开发者能够用它将自定义的任务(task)追加到适当的派发队列(dispatch queue),就能生成必要的线程并执行任务。
在 GCD 中有三种队列:主队列(main queue)、全局队列(global queue)、用户队列(user-created queue)。全局队列是并发队列,即队列中的任务(task)执行顺序和进入队列的顺序无关;主队列和用户队列是串行队列,队列中的任务按FIFO(first input first output,先进先出)的顺序执行。
GCD 有两种派发方式:同步派发和异步派发。千万注意:这里的同步和异步指的是 “任务派发方式”,而非任务的执行方式。
看个例子:
// 这小段代码有问题,出现了线程死锁,知道为何吗? // 提示:下面的代码在主线程(main_thread)中执行 - (void)viewDidLoad { dispatch_sync(dispatch_get_main_queue(), block()); }
要理解这题,首先须要了解 dispatch_sync 和 dispatch_async 的工做流程。
dispatch_sync(queue, block) 作了两件事情
- 将 block 添加到 queue 队列;
- 阻塞调用线程,等待 block() 执行结束,回到调用线程。
dispatch_async(queue, block) 也作了两件事情:
- 将 block 添加到 queue 队列;
- 直接回到调用线程(不阻塞调用线程)。
这里也能看到同步派发和异步派发的区别,就是看是否阻塞调用线程。
回到题目,当在 main_thread 中调用 dispatch_sync 时:
- main_thread 被阻塞,没法继续执行;
- 同步派发 sync 致使 block() 须要在 main_thread 中执行结束才会返回;
- 而此时 main_thread 被阻塞,二者互相等待,线程死锁;
因此记住这个教训:不要将 block 同步派发到调用 GCD 所在线程的关联队列中。例如,若是你在主线程(main thread)中调用 GCD,那么在 GCD 内就不要使用同步派发(dispatch_sync)将 block 派发到主线程(main thread)关联的主队列(main queue)中。
除此以外,还有个容易让人忽略而致使死锁的东西:队列的层级体系。
// 因最外层 queueA 已经同步派发,致使内层 queueA 同步派发时会死锁 // 这个例子同时也告诫咱们不要相信和使用 dispatch_get_current_queue dispatch_sync (queueA, ^{ dispatch_block_t block = ^{ if (dispatch_get_current_queue() == queueA) { block(); } else { dispatch_sync(queueA, block); } } })
队列层级用图画出来一般长这样,最顶层是全局并发队列(此图和上面例子无关)
有了前面的基础,就能够瞧瞧在 GCD 中更好的同步锁的实现方式。在 GCD 队列中,有个简单直接的方法能够代替同步锁或锁对象,将读写操做都安排在一个串行同步队列里,便可保证数据同步,以下:
_syncQueue = dispatch_queue_create("com.effectiveObjectiveC.syncQueue", NULL); - (NSString *)someString { __weak NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString *)someString { dispatch_sync(_syncQueue, ^{ _someString = someString; }); }
使用串行同步队列,将读写操做所有放在序列化的队列里执行,全部指针对属性的操做便可同步。加锁和解锁的所有转移给 GCD 处理,而 GCD 在较深的底层实现,能够进行许多的优化。
然而设置方法不必定非得是同步的,设置实例变量的 block 没有返回值,因此能够将此方法改为异步:
- (void)setSomeString:(NSString *)someString { dispatch_async(_syncQueue, ^{ _someString = someString; }); }
此次只是把 dispatch_sync 改为 dispatch_async,从调用者来看提高了执行速度。但正是因为执行异步派发
dispatch_async 时会拷贝 block,当拷贝 block 的时间大于执行 block 的时间时,dispatch_async 的速度会比 dispatch_sync 速度更慢。因此实际状况应根据 block 所执行任务的繁重程度来决定使用 dispatch_async 仍是 dispatch_sync。
多个获取方法能够并发执行,获取方法与设置方法不能并发执行。据此可使用并发队列和 GCD 的 barrier 来写出更快的代码。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - (NSString *)someString { __weak NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString *)someString { // barrier dispatch_barrier_async(_syncQueue, ^{ _someString = someString; }); }
在使用上面的方式建立的同步锁以后,会发现执行速度和效率都更高。难道并发队列厉害吗?其实缘由不仅是并发队列,还有 barrier block 的功劳,那么什么是 barrier block 呢?
函数 dispatch_barrier_sync 和 dispatch_barrier_async 可让队列中派发的 block 变成 barrier(栅栏) 使用,这种 block 称为 barrier block。队列中的 barrier block 必须等当前并发队列中的 block 都执行结束才开始执行,时序图以下:
GCD 的同步方式还有组派发(dispatch group)和信号量(dispatch semaphore)
/** * 阻塞当前线程,执行group内任务,阻塞时间为timeout * * @param group 等待的group * @param timeout 等待的时间,即函数在等待dispatch group内的任务执行完毕时,应阻塞多久 * * @return 若是执行dispatch group所需时间小于timeout,则返回0,不然返回非0值; timeout能够取常量DISPATCH_TIME_FOREVER,表示永远不会超时 */ long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
/** * 若是group内的任务所有执行完毕后,将block提交到queue上执行 * * @param group 等待的group * @param queue 即将提交的队列 * @param block 即将提交的任务 */ void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
信号量在 linux/unix 开发中十分常见,其概念至关于经典的“生产者-消费者”模型。当信号个数为 0 时,则线程阻塞,等待发送新信号;一旦信号个数大于 0 时,就开始处理任务。
dispatch_semaphore_create:建立一个semaphore
dispatch_semaphore_signal:发送一个信号,信号个数加1
dispatch_semaphore_wait:等待信号
除了《Effective Objective-C 2.0》以外,本文还参考了:
[0] 百度百科:线程安全的基本概念
[1] 老谭:Objective-C 中不一样方式实现锁(一)
[2] 老谭:Objective-C 中不一样方式实现锁(二)
[4] 老谭:在 CGD 中快速实现多线程的并发控制
[5] 飘飘白云:深刻浅出 Cocoa 多线程编程之 block 与 dispatch quene