在多线程编程中,最多见的场景是如何保证线程安全,好比你可能常常遇到多线程访问某个dic(又或者是array或其余)形成的crash。
这篇文章里,咱们讨论下如何使用GCD实现多线程读者与写者问题,也即单一资源的线程安全问题。
同时会有一些在MRC下crash问题讨论。git
ARC版本github
_ioQueue = dispatch_queue_create("ioQueue", DISPATCH_QUEUE_CONCURRENT); - (void)setSafeObject:(id)object forKey:(NSString *)key { key = [key copy]; dispatch_barrier_async(self.ioQueue, ^{ if (key && object) { [_dic setObject:object forKey:key]; } }); } - (id)getSafeObjectForKey:(NSString *)key { __block id result = nil; dispatch_sync(self.ioQueue, ^{ result = [_dic objectForKey:key]; }); return result; }
首先,咱们须要建立一个私有的并行
队列来处理读写操做。
在这里不该该使用globe_queue
, 由于咱们经过dispatch_barrier_async
来保证写操做的互斥,咱们不但愿写操做阻塞住globe_queue
中的其余不相关任务,咱们只但愿在写的同时,不会有其余的写操做或者读操做。
同时,也不推荐给队列设置优先级,多数状况下使用default就能够了。而改变优先级每每会形成一些没法预料的问题,好比优先级反转(具体的能够参看参考文献)。编程
dispatch_barrier_async
的block运行时机是,在它以前全部的任务执行完毕,而且在它后面的任务开始以前,期间不会有其余的任务执行。注意在barrier执行的时候,队列本质上如同一个串行队列,其执行完之后才会恢复到并行队列。
安全
另一个值得注意的问题是,在写操做的时候,咱们使用dispatch_async
,而在读操做的时候咱们使用dispatch_sync
。很明显,这2个操做一个是异步的,一个是同步的。咱们不须要使每次程序执行的时候都等待写操做完成,因此写操做异步执行,可是咱们须要同步的执行读操做来保证程序可以马上获得它想要的值。
多线程
使用sync的时候须要极其的当心,由于稍不注意,就有可能产生死锁,这可能形成灾难性的后果。你确定也注意到了在写操做的时候对key进行了copy, 关于此处的解释,插入一段来自参考文献的引用:并发
函数调用者能够自由传递一个
NSMutableString
的key,而且可以在函数返回后修改它。所以咱们必须对传入的字符串使用copy
操做以确保函数可以正确地工做。若是传入的字符串不是可变的(也就是正常的NSString
类型),调用copy基本上是个空操做。异步
到这里整个基本示例代码已经完成,通常状况下可以知足咱们的须要。下面来看看在MRC过程当中我遇到的一些问题。async
dispatch_queue_t queueA; // 串行队列 dispatch_sync(queueA, ^(){ dispatch_sync(queueA, ^(){ foo(); }); });
形成死锁比较常见的状况能够简化成上面这段代码。dispatch_sync
会同步的提交工做并在返回前等待其完成。第一dispatch_sync
正在运行并等待它的block完成,可是block不可以完成,它调用了第二个dispatch_sync
,而第二个dispatch_sync
会等待串行队列中已经存在的第一个任务完成,很明显这个任务没法完成,形成死锁。函数
值得注意的是main_queue就是一个串行队列。
- (void)setSafeObject:(id)object forKey:(NSString *)key { key = [key copy]; dispatch_barrier_async(self.ioQueue, ^{ if (key && object) { [_dic setObject:object forKey:key]; } }); [key release]; } - (id)getSafeObjectForKey:(NSString *)key { __block id result = nil; dispatch_sync(self.ioQueue, ^{ result = [_dic objectForKey:key]; }); return result; }
首先咱们看看上面这段代码,基本就是ARC版本转换过来的,看起来没问题。那么到底是不是真的没问题,咱们跑段代码试试看:oop
//版本一 - (void)test { for (int i = 0; i < 1000000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self setSafeObject:[NSString stringWithFormat:@"86+131633829%i", i] forKey:KEY]; }); NSString *result = [self getSafeObjectForKey:KEY]; NSLog(@"get string: %@, length : %lu", result, result.length); } }
test执行后,很快就会发生crash,读操做的result会发生野指针。
若是你有经验的话,可能会发现问题:
若是某个线程a刚取出了result值,此次线程b开始执行写操做,形成线程a中的result值成为了一份过时的数据,若是正好线程b的runloop结束,颇有可能旧的result内存地址被释放掉,这时线程a中的result就会发生野指针crash。
这时候,你可能会采起这样子的修改,代码以下:
//版本二 - (void)test { for (int i = 0; i < 1000000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self setSafeObject:[NSString stringWithFormat:@"86+131633829%i", i] forKey:KEY]; }); NSString *result = [[self getSafeObjectForKey:KEY] retain]; NSLog(@"get string: %@, length : %lu", result, result.length); [result release]; } }
运行以后会发现,仍然会crash,其实问题和上面同样,咱们的改动没有真正的解决问题。最好的解决方案是在读操做以前就已经retain
住了,看看最终版的代码吧:
//最终版 - (id)getSafeObjectForKey:(NSString *)key { __block id result = nil; dispatch_sync(self.ioQueue, ^{ result = [[_dic objectForKey:key] retain]; }); return [result autorelease]; }
注意retain过必定要释放掉,否则或形成内存泄露。
再次验证后发现,程序不会crash了。
转载请注明出处哦,个人博客: luoyibu
GCD是一套很好用的多线程库,更多的用法请看参考资料