iOS并发编程Tips(二)

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

  • 线程安全
  • 使用主线程
  • GCD 还是 NSOperationQueue

线程安全

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

举个例子。

我们定义一个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

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

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

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

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

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

上面提到了锁,我们常用的锁有很多,比如,互斥锁、条件锁、递归锁、信号量、自旋锁等等。网上有很多关于这方面的资料,我就不再赘述了,毕竟篇幅很大,而我这篇只是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方法上打个断点,我们会看到:

这样就会有问题,如果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也是一种不错的选择。

Article Published in on iOS

Article by 付军