iOS并发编程Tips(一)
DieterNPC
8年前
<p> </p> <p>关于iOS并发编程, <a href="http://www.open-open.com/lib/view/open1460440929522.html" rel="nofollow,noindex">雷纯锋有篇</a>做了很完整的介绍,大家可以移步学习一下。</p> <p>我们在这里并不探究 NSThread 、 GCD 、 NSOperation 、 NSOperationQueue 的具体用法,只探讨一些容易被遗忘的小点。</p> <h2>线程成本</h2> <p>首先,什么是线程,维基百科上是这么说的:</p> <p>A thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler.</p> <p>按照雷纯锋的博客上的说法就是:</p> <p>线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads。</p> <p>在iOS中,进程启动之后,一个最主要的线程我们称为主线程。主线程会创建和管理所有的UI元素。一般来说,与用户交互相关的中断性操作都会派发到主线程上进行处理,包括你的 <strong>IBAction</strong> 的方法。</p> <p>线程的创建是需要成本的,每个线程不仅仅在创建的过程中需要耗费时间,同时,它也会占用一定的内核的内存空间和app的内存空间。</p> <p>Each thread has its own execution stack and is scheduled for runtime separately by the kernel. — Apple Thread Management</p> <h3>内核数据结构(Kernel data structures)</h3> <p>按照 <a href="/misc/goto?guid=4959670271755834205" rel="nofollow,noindex">苹果官方文档</a> 上的说法,每个线程在内核空间上大概要消耗 <strong>1KB</strong> 大小的内存。而这块内存是用于存储线程的数据结构和属性的。这是一个连系内存(wired memory),不能在磁盘上分页。</p> <p>This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk.</p> <h3>线程栈空间大小(Stack space)</h3> <p>在iOS中,主线程的栈空间大小为 <strong>1MB</strong> , 在OS X中,主线程的栈空间大小为 <strong>8MB</strong> ,并且,这都是不可修改的。子线程默认栈空间为 <strong>512KB</strong> 。</p> <p>栈空间不是立即被创建分配的,它会随着使用而增长。所以说,即使主线程有 <strong>1MB</strong> 的栈空间,那么,在很大的一段时间之内,你都只会用到很少的一部分。</p> <p>子线程允许分配的最小栈空间是 <strong>16KB</strong> ,并且,必须为 <strong>4KB</strong> 的倍数。我们可以通过 stackSize 属性来修改一个子线程的栈空间:</p> <pre> <code class="language-objectivec">NSThread *t = [[NSThread alloc] initWithTarget:target selector:selector object:object]; t.stackSize = size; </code></pre> <h3>线程创建时间(Creation time)</h3> <p>The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5.</p> <p>按照 <a href="/misc/goto?guid=4959670271755834205" rel="nofollow,noindex">苹果官方文档</a> 的说法,在一个2GHz的双核Intel处理器、1GB内存、OS X 10.5系统的iMac上,需要花费 <strong>90微秒</strong> 的时间(有些人会写90ms或者是90毫秒,其实,这里的ms是microsecond,而不是millisecond)。</p> <h2>原子属性</h2> <p>在声明属性的时候,我们两种选择,一种是 atomic ,一种是 nonatomic ,前者是原子的,后者是非原子的。基本上,他们的区别就在于, atomic 会在属性的 setter 方法上加上一个互斥锁(也 <a href="/misc/goto?guid=4959670271869109322" rel="nofollow,noindex">有一种说法</a> 是使用自旋锁spin locks,不过,由于 <a href="/misc/goto?guid=4958978318529107162" rel="nofollow,noindex">自旋锁的bug</a> ,可能苹果并不会使用自旋锁,转而使用 pthread_mutex 或者 dispatch_semaphore 等):</p> <p>atomic</p> <pre> <code class="language-objectivec">- (void)setCurrentImage:(UIImage *)currentImage { @synchronized(self) { if (_currentImage != currentImage) { _currentImage = currentImage; } } } - (UIImage *)currentImage { @synchronized(self) { return _currentImage; } } </code></pre> <p>nonatomic</p> <pre> <code class="language-objectivec">- (void)setCurrentImage:(UIImage *)currentImage { if (_currentImage != currentImage) { _currentImage = currentImage; } } - (UIImage *)currentImage { return _currentImage; } </code></pre> <p>属性默认是 atomic 修饰的,明确写 nonatomic 才会是非原子操作。</p> <p>比如:</p> <pre> <code class="language-objectivec">@property(nonatomic, strong) UITextField *userName; @property(atomic, strong) UITextField *userName; @property(strong) UITextField *userName; </code></pre> <p>后两者其实是一样的,只有第一种才是非原子操作。</p> <h3>是否线程安全?</h3> <p>从上面的代码来看, atomic 最多也就只能保证属性的 setter 和 getter 方法是线程安全的。</p> <p>我们举个例子,如果现在同时发生:</p> <ul> <li>线程A在调用 getter 方法。</li> <li>线程B、线程C在调用 setter 方法,并且它们设置的值是不一致的。</li> </ul> <p>那么,线程A可能会获得原来的值,也可能会获得线程B或者线程C的值,这是不一定的。而且,属性最终的值可能是线程B,也可能是线程C设置的值。</p> <p>用《 <a href="/misc/goto?guid=4959670271973495443" rel="nofollow,noindex">Effective Objective-C 2.0</a> 》上面的话说,就是:</p> <p>这么做虽然能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对是线程安全的。当然,访问属性的操作确实是“原子”的。使用属性时,必定能从中获取到有效值,然而在同一个线程上多次调用获取方法,每次获取到的结果却未必相同。在两次访问操作之间,其他线程可能会写入新的值。</p> <p>所以,要说到真正的线程安全, atomic 的差距还是有点大的。</p> <h3>是否应该使用?</h3> <p>在没有资源竞争的情况下(比如,单线程的时候), atomic 可能还是很快的,但是 在比较普遍的情况下, atomic 想比起 nonatomic 可能会有靠近20倍的性能差异, <a href="/misc/goto?guid=4959670271869109322" rel="nofollow,noindex">stack overflow中有人对此进行了测试</a> 。</p> <p>那么,究竟是否该使用 atomic 呢,这个要看你是否需要。对我来说,我一般很少使用 atomic ,如果实在有需要的话,我一般会使用 dispatch_barrier 代替(具体例子可以参考下面的 dispatch_barrier 的 setter 和 getter 的写法)。</p> <h2>并发同步</h2> <p>在GCD上,我们有两种常见方法来让并发程序在某个点上进行同步,分别是 dispatch_group 和 dispatch_barrier 。相比起 dispatch_barrier ,我们可能用到 dispatch_group 的地方会更多一些。</p> <p>dispatch_group 允许向 group 中添加多个block块,在所有添加的block块全部执行完成之后,再通知其他队列执行其他的方法。而这个完成点就是并发的同步点。</p> <p>dispatch_group 的写法一般如下:</p> <pre> <code class="language-objectivec">dispatch_queue_t dispatchQueue = dispatch_queue_create("com.ifujun.text", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t dispatchGroup = dispatch_group_create(); dispatch_group_async(dispatchGroup, dispatchQueue, ^(){ NSLog(@"dispatch-1"); }); dispatch_group_async(dispatchGroup, dispatchQueue, ^(){ NSLog(@"dspatch-2"); }); dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){ NSLog(@"end"); }); </code></pre> <p>dispatch_barrier 就比较有意思了。 dispatch_barrier 是一个障碍点,在并发队列遇到 dispatch_barrier 之后, dispatch_barrier 的block块会被延迟执行,直到所有在它之前提交的block块全部执行完成,然后才会开始执行 dispatch_barrier 的block块。</p> <p>我们举个例子:</p> <pre> <code class="language-objectivec">dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 1"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 2"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 3"); }); dispatch_barrier_async(concurrentQueue, ^(){ NSLog(@"barrier"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 4"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 5"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 6"); }); </code></pre> <p>上面的代码中,block 1 - 6 都是可以并发执行的,但是由于 barrier 的存在,在block 1 - 3 执行完成之后,才会执行 barrier ,在 barrier 执行完成之后,才会并发执行剩下的block 4 - 6。</p> <p>执行顺序如下图:</p> <p><img src="https://simg.open-open.com/show/113aaa3bdaf490b80de1f27daa6bdc62.png"></p> <p>dispatch_barrier 有一个比较常见的用法是读写锁。在上面的 atomic 上,我们说到, atomic 因为给 setter 和 getter 方法加锁,会造成很大的性能浪费,相当于同时只能一个线程在读或者写。</p> <p>我们要的并不是单读单写,我们要的是多读单写,这样才能确保数据完整并且性能不错。</p> <p>我们这以缓存举例(缓存必然需要有较高的性能,同时也要支持多读单写),如果用 dispatch_barrier 来实现的话,大概会是这样:</p> <pre> <code class="language-objectivec">#import "FKCache.h" @interface FKCache () @property (strong, nonatomic) NSMutableDictionary *cacheDictionary; @property (strong, nonatomic) dispatch_queue_t queue; @end @implementation FKCache + (instancetype)shardInstance { static FKCache *cache = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [[FKCache alloc] init]; }); return cache; } - (instancetype)init { if (self = [super init]) { _cacheDictionary = [NSMutableDictionary dictionary]; _queue = dispatch_queue_create("com.ifujun.readwritelock", DISPATCH_QUEUE_CONCURRENT); } return self; } - (void)setObjectForKey:(id)object forKey:(NSString *)key { dispatch_barrier_async(self.queue, ^{ [self.cacheDictionary setObject:object forKey:key]; }); } - (id)objectForKey:(NSString *)key { __block id value = nil; dispatch_async(self.queue, ^{ value = [self.cacheDictionary objectForKey:key]; }); return value; } @end </code></pre> <p>注意</p> <p>和 dispatch_group 比起来有一点很大的不同的是, dispatch_group 上添加的block块可以来自于 <strong>不同的并发队列</strong> ,而 dispatch_barrier 只会阻塞 <strong>同一个并发队列</strong> 中的block。</p> <h2>参考文档</h2> <ol> <li><a href="/misc/goto?guid=4959670271755834205" rel="nofollow,noindex">https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html</a></li> <li><a href="/misc/goto?guid=4959670272082332547" rel="nofollow,noindex">http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/</a></li> <li><a href="/misc/goto?guid=4959670272170369396" rel="nofollow,noindex">http://stackoverflow.com/questions/588866/whats-the-difference-between-the-atomic-and-nonatomic-attributes/589392#589392</a></li> </ol> <p>来自: <a href="/misc/goto?guid=4959670272253188463" rel="nofollow">http://ifujun.com/iosbing-fa-bian-cheng-tips/</a> </p>