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>