转载

iOS并发编程Tips(二)

博文链接: http://ifujun.com/iosbing-fa-bian-cheng-tips-er/

在 iOS并发编程Tips(一) 中,我们提到了三点,分别是线程、原子属性和并发同步。在本文中,你将会看到以下几点:

  • 线程安全

  • 使用主线程

  • GCD 还是 NSOperationQueue

线程安全

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 — 维基百科

举个例子。

iOS并发编程Tips(二)

我们定义一个 NSInteger 型的全局变量 count ,我们使用三个异步线程将它自增100000,那么,我们希望的输出结果是300000。但是,它的真实结果是多少呢?

#import "ViewController.h"  @interface ViewController () @property (assign, nonatomic) NSInteger count; @end  @implementation ViewController  -(void)viewDidLoad {     [super viewDidLoad];      for (int i = 0; i < 3; i++)     {         [self startThread];     } } -(void)startThread {     dispatch_async(dispatch_get_global_queue(0, 0), ^{         [self addCount];     }); } -(void)addCount {     for (int i = 0; i < 100000; i++)     {         self.count++;     }     NSLog(@"count = %ld", self.count); } -(void)addCountWithLock {     @synchronized (self)     {         for (int i = 0; i < 100000; i++)         {             self.count++;         }         NSLog(@"lock count = %ld", self.count);     } } @end 

运行结果显然不是我们想要的,而且,每次的结果都不一定一致,这就是我们所要说的线程安全。

iOS并发编程Tips(二)

很多时候,我们为了效率,会编写多线程的代码。多线程除了会带来效率的提升之外,也会提高控制的复杂程度。我们有很多解决办法,比如说,使用锁、不可变变量、尽量使用主线程(单线程)等等。

在上述例子中,我们如果加一个最简单的互斥锁( addCountWithLock 方法),就可以达到线程安全的目的。

iOS并发编程Tips(二)

运行结果正是我们想要的。

还有一点想提及一下的是, 苹果有个文档 列出了部分框架的部分安全和非安全的类和函数,可以适当看一下。

上面提到了锁,我们常用的锁有很多,比如,互斥锁、条件锁、递归锁、信号量、自旋锁等等。网上有很多关于这方面的资料,我就不再赘述了,毕竟篇幅很大,而我这篇只是Tips。

网上也有很多关于这些锁性能对比的文章,比如说 ibireme的文章 等等。

这么多锁,除了比较特殊的递归锁等,如果你想要一个高性能的锁的话,可以使用 pthread_mutex 或者 dispatch_semaphore ,如果想使用比较方便的话,以直接使用 @synchronizedNSLock

使用主线程

在性能优化的时候,我们很容易陷入过度优化的误区。现在的设备性能越来越好,我们可以在主线程中做越来越多的事情。

如果某个函数或者方法只有主线程去访问,那它必然是多线程安全的,因为只有单线程访问,不存在多线程的情况。

我们知道 NSMutableArrayNSMutableDictionary 这种的是非线程安全的类,在我的使用过程中,我一般不会对这些东西加锁,因为我基本只用主线程去访问,而如果涉及到多线程的话,我会使用不可变的数组和字典。

在大多数情况下,使用多线程只存在于某一个部分,比如网络等,那么在多线程执行完成之后,一定要交由主线程回调。比如,我们常用的 AFNetworking 中,在回调 successfailure 的block块的过程中,就会回调到主线程上:

dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{                             success(self, responseObject);                         }); 

除了我们自己设计的库需要这么做以外,也有一些系统上的方法需要我们注意。比如, NSNotification

NSNotification 是哪个线程去post就是哪个线程去调用 selector 。我们来测试一下:

[[NSNotificationCenter defaultCenter] addObserver:self                                              selector:@selector(test)                                                  name:kTestNotification                                                object:nil];      dispatch_async(dispatch_get_global_queue(0, 0), ^{     [[NSNotificationCenter defaultCenter] postNotificationName:kTestNotification object:nil]; }); 

我们在 test 方法上打个断点,我们会看到:

iOS并发编程Tips(二)

这样就会有问题,如果 test 方法内是执行UI操作或者某些需要主线程的操作的话,那么有可能会造成UI无响应,或者很长时间才变化,甚至是崩溃。

所以,我建议一定要在主线程上post,因为你不知道你所发出的 NSNotification 谁会去接收,它又要去干什么,但是你知道,主线程是肯定没错的。

实现这个的方法有很多,比如继承、category、hook等。

前段时间在写指纹解锁的时候碰到一个问题。在我的App中需要验证指纹或者手势密码才可以进入主页,而验证指纹需要用到这么一个方法:

-(void)evaluatePolicy:(LAPolicy)policy        localizedReason:(NSString *)localizedReason                  reply:(void(^)(BOOL success, NSError * __nullable error))reply; 

测试的时候,我发现一个问题,在用户验证通过之后, alertView 消失之后,页面并没有跳到主页。有时候需要过好久才会跳到主页,但是页面并没有卡死,手势解锁依旧可用。这就奇了怪了,我找了一圈才发现,这个方法是在子线程上回调回来的,而我并不知道。所以我用这个子线程去初始化页面的时候,就会出现长时间无响应的问题。

所以,系统异步回调的接口一定要去检查一下是不是主线程的。

GCD 还是 NSOperationQueue

我们知道,在 iOS 4 以上, NSOperationQueue 是在GCD上封装上来的,相比起GCD, NSOperationQueue 具有如下一些优点:

  • 提供cancel操作。

  • 更细粒度的优先级控制。

  • 支持继承,方便封装。

  • 支持KVO。

而GCD相比起 NSOperationQueue 的优点是:

  • 使用方便、简单。

  • 速度可能更快一点。

我相信,对于大部分好的封装来说,会优先选择 NSOperationQueue 。而如果你只是一个很小的项目,以使用方便为主,那么,使用GCD也是一种不错的选择。

原文  https://segmentfault.com/a/1190000005072840
正文到此结束
Loading...