IOS 多线程04-GCD详解 底层并发 API

注:本人是翻译过来,而且加上本人的一点看法。html

 

前言算法

想要揭示出表面之下深层次的一些可利用的方面。这些底层的 API 提供了大量的灵活性,随之而来的是大量的复杂度和更多的责任。在咱们的文章常见的后台实践中提到的高层的 API 和模式可以让你专一于手头的任务而且免于大量的问题。一般来讲,高层的 API 会提供更好的性能,除非你能承受起使用底层 API 带来的纠结于调试代码的时间和努力。编程

尽管如此,了解深层次下的软件堆栈工做原理仍是有颇有帮助的。咱们但愿这篇文章可以让你更好的了解这个平台,同时,让你更加感谢这些高层的 API。数组

首先,咱们将会分析大多数组成 Grand Central Dispatch 的部分。它已经存在了好几年,而且苹果公司持续添加功能而且改善它。如今苹果已经将其开源,这意味着它对其余平台也是可用的了。最后,咱们将会看一下原子操做——另外的一种底层代码块的集合。缓存

或许关于并发编程最好的书是 M. Ben-Ari 写的《Principles of Concurrent Programming》,ISBN 0-13-701078-8。若是你正在作任何与并发编程有关的事情,你须要读一下这本书。这本书已经30多年了,仍然很是卓越。书中简洁的写法,优秀的例子和练习,带你领略并发编程中代码块的基本原理。这本书如今已经绝版了,可是它的一些复印版依然广为流传。有一个新版书,名字叫《Principles of Concurrent and Distributed Programming》,ISBN 0-321-31283-X,好像有不少相同的地方,不过我尚未读过。安全

 

目录:性能优化

1. 从前
2. 延后执行
3. 队列
4. 目标队列
5. 资源保护
6. 单一资源的多读单写
7. 锁竞争
8. 全都使用异步分发
9. 如何写出好的异步 API
10. 迭代执行
11. 组
12. 对现有API使用 dispatchgroupt
13. 事件源
14. 监视进程
15. 监视文件
16. 定时器
17. 取消
18. 输入输出
19. GCD 和缓冲区
20. 读和写
21. 基准测试
22. 原子操做
23. 计数器
24. 比较和交换
25. 原子队列
26. 自旋锁网络

 

1. 从前多线程

  或许GCD中使用最多而且被滥用功能的就是 dispatch_once 了。正确的用法看起来是这样的:并发

+ (UIColor *)boringColor;
{
    static UIColor *color;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        color = [UIColor colorWithRed:0.380f green:0.376f blue:0.376f alpha:1.000f];
    });
    return color;
}

  上面的 block 只会运行一次。而且在连续的调用中,这种检查是很高效的。你能使用它来初始化全局数据好比单例。要注意的是,使用 dispatch_once_t 会使得测试变得很是困难(单例和测试不是很好配合)。

  要确保 onceToken 被声明为 static ,或者有全局做用域。任何其余的状况都会致使没法预知的行为。换句话说,不要dispatch_once_t 做为一个对象的成员变量,或者相似的情形。

  退回到远古时代(其实也就是几年前),人们会使用 pthread_once ,由于 dispatch_once_t 更容易使用而且不易出错,因此你永远都不会再用到 pthread_once 了。

 

2. 延后执行

  另外一个常见的小伙伴就是 dispatch_after 了。它使工做延后执行。它是很强大的,可是要注意:你很容易就陷入到一堆麻烦中。通常用法是这样的:

- (void)foo
{
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self bar];
    });
}

  第一眼看上去这段代码是极好的。可是这里存在一些缺点。咱们不能(直接)取消咱们已经提交到 dispatch_after 的代码,它将会运行。

  另一个须要注意的事情就是,当人们使用 dispatch_after 去处理他们代码中存在的时序 bug 时,会存在一些有问题的倾向。一些代码执行的过早而你极可能不知道为何会这样,因此你把这段代码放到了 dispatch_after 中,如今一切运行正常了。可是几周之后,以前的工做不起做用了。因为你并不十分清楚你本身代码的执行次序,调试代码就变成了一场噩梦。因此不要像上面这样作。大多数的状况下,你最好把代码放到正确的位置。若是代码放到 -viewWillAppear 太早,那么或许 -viewDidAppear 就是正确的地方。

  经过在本身代码中创建直接调用(相似 -viewDidAppear )而不是依赖于 dispatch_after ,你会为本身省去不少麻烦。

  若是你须要一些事情在某个特定的时刻运行,那么 dispatch_after 或许会是个好的选择。确保同时考虑了 NSTimer,这个API虽然有点笨重,可是它容许你取消定时器的触发。

 

3. 队列

  GCD 中一个基本的代码块就是队列。下面咱们会给出一些如何使用它的例子。当使用队列的时候,给它们一个明显的标签会帮本身很多忙。在调试时,这个标签会在 Xcode (和 lldb)中显示,这会帮助你了解你的 app 是由什么决定的:

- (id)init;
{
    self = [super init];
    if (self != nil) {
        NSString *label = [NSString stringWithFormat:@"%@.isolation.%p", [self class], self];
        self.isolationQueue = dispatch_queue_create([label UTF8String], 0);

        label = [NSString stringWithFormat:@"%@.work.%p", [self class], self];
        self.workQueue = dispatch_queue_create([label UTF8String], 0);
    }
    return self;
}

  队列能够是并行也能够是串行的。默认状况下,它们是串行的,也就是说,任何给定的时间内,只能有一个单独的 block 运行。这就是隔离队列(原文:isolation queues。译注)的运行方式。队列也能够是并行的,也就是同一时间内容许多个 block 一块儿执行。

  GCD 队列的内部使用的是线程。GCD 管理这些线程,而且使用 GCD 的时候,你不须要本身建立线程。可是重要的外在部分 GCD 会呈现给你,也就是用户 API,一个很大不一样的抽象层级。当使用 GCD 来完成并发的工做时,你没必要考虑线程方面的问题,取而代之的,只需考虑队列和功能点(提交给队列的 block)。虽然往下深究,依然都是线程,可是 GCD 的抽象层级为你惯用的编码提供了更好的方式。

  队列和功能点同时解决了一个接二连三的扇出的问题:若是咱们直接使用线程,而且想要作一些并发的事情,咱们极可能将咱们的工做分红 100 个小的功能点,而后基于可用的 CPU 内核数量来建立线程,假设是 8。咱们把这些功能点送到这 8 个线程中。当咱们处理这些功能点时,可能会调用一些函数做为功能的一部分。写那个函数的人也想要使用并发,所以当你调用这个函数的时候,这个函数也会建立 8 个线程。如今,你有了 8 × 8 = 64 个线程,尽管你只有 8 个CPU内核——也就是说任什么时候候只有12%的线程实际在运行而另外88%的线程什么事情都没作。使用 GCD 你就不会遇到这种问题,当系统关闭 CPU 内核以省电时,GCD 甚至可以相应地调整线程数量。

  GCD 经过建立所谓的线程池来大体匹配 CPU 内核数量。要记住,线程的建立并非无代价的。每一个线程都须要占用内存和内核资源。这里也有一个问题:若是你提交了一个 block 给 GCD,可是这段代码阻塞了这个线程,那么这个线程在这段时间内就不能用来完成其余工做——它被阻塞了。为了确保功能点在队列上一直是执行的,GCD 不得不建立一个新的线程,并把它添加到线程池。

  若是你的代码阻塞了许多线程,这会带来很大的问题。首先,线程消耗资源,此外,建立线程会变得代价高昂。建立过程须要一些时间。而且在这段时间中,GCD 没法以全速来完成功能点。有很多可以致使线程阻塞的状况,可是最多见的状况与 I/O 有关,也就是从文件或者网络中读写数据。正是由于这些缘由,你不该该在GCD队列中以阻塞的方式来作这些操做。看一下下面的输入输出段落去了解一些关于如何以 GCD 运行良好的方式来作 I/O 操做的信息。

 

4. 目标队列

  你可以为你建立的任何一个队列设置一个目标队列。这会是很强大的,而且有助于调试。

  为一个类建立它本身的队列而不是使用全局的队列被广泛认为是一种好的风格。这种方式下,你能够设置队列的名字,这让调试变得轻松许多—— Xcode 可让你在 Debug Navigator 中看到全部的队列名字,若是你直接使用 lldb(lldb) thread list 命令将会在控制台打印出全部队列的名字。一旦你使用大量的异步内容,这会是很是有用的帮助。

  使用私有队列一样强调封装性。这时你本身的队列,你要本身决定如何使用它。

  默认状况下,一个新建立的队列转发到默认优先级的全局队列中。咱们就将会讨论一些有关优先级的东西。

  你能够改变你队列转发到的队列——你能够设置本身队列的目标队列。以这种方式,你能够将不一样队列连接在一块儿。你的 Foo 类有一个队列,该队列转发到 Bar 类的队列,Bar 类的队列又转发到全局队列。

  当你为了隔离目的而使用一个队列时,这会很是有用。Foo 有一个隔离队列,而且转发到 Bar 的隔离队列,与 Bar 的隔离队列所保护的有关的资源,会自动成为线程安全的。

  若是你但愿多个 block 同时运行,那要确保你本身的队列是并发的。同时须要注意,若是一个队列的目标队列是串行的(也就是非并发),那么实际上这个队列也会转换为一个串行队列。

 

5. 资源保护

  多线程编程中,最多见的情形是你有一个资源,每次只有一个线程被容许访问这个资源。它一般就是一块内存或者一个对象,每次只有一个线程能够访问它。

  举例来讲,咱们须要以多线程(或者多个队列)方式访问 NSMutableDictionary 。咱们可能会照下面的代码来作:

- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
    key = [key copy];
    dispatch_async(self.isolationQueue, ^(){
        if (count == 0) {
            [self.counts removeObjectForKey:key];
        } else {
            self.counts[key] = @(count);
        }
    });
}

- (NSUInteger)countForKey:(NSString *)key;
{
    __block NSUInteger count;
    dispatch_sync(self.isolationQueue, ^(){
        NSNumber *n = self.counts[key];
        count = [n unsignedIntegerValue];
    });
    return count;
}

  经过以上代码,只有一个线程能够访问 NSMutableDictionary 的实例。

  注意如下四点:

  1. 不要使用上面的代码,请先阅读多读单写锁竞争
  2. 咱们使用 async 方式来保存值,这很重要。咱们不想也没必要阻塞当前线程只是为了等待写操做完成。当读操做时,咱们使用 sync由于咱们须要返回值。
  3. 从函数接口能够看出,-setCount:forKey: 须要一个 NSString 参数,用来传递给 dispatch_async。函数调用者能够自由传递一个 NSMutableString 值而且可以在函数返回后修改它。所以咱们必须对传入的字符串使用 copy 操做以确保函数可以正确地工做。若是传入的字符串不是可变的(也就是正常的 NSString 类型),调用copy基本上是个空操做。
  4. isolationQueue 建立时,参数 dispatch_queue_attr_t 的值必须是DISPATCH_QUEUE_SERIAL(或者0)。

 

6. 单一资源的多读单写

  咱们可以改善上面的那个例子。GCD 有可让多线程运行的并发队列。咱们可以安全地使用多线程来从 NSMutableDictionary 中读取只要咱们不一样时修改它。当咱们须要改变这个字典时,咱们使用 barrier 来分发这个 block。这样的一个 block 的运行时机是,在它以前全部计划好的 block 完成以后,而且在全部它后面的 block 运行以前。

  以以下方式建立队列:

self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);

  而且用如下代码来改变setter函数:

- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
    key = [key copy];
    dispatch_barrier_async(self.isolationQueue, ^(){
        if (count == 0) {
            [self.counts removeObjectForKey:key];
        } else {
            self.counts[key] = @(count);
        }
    });
}

当使用并发队列时,要确保全部的 barrier 调用都是 async 的。若是你使用 dispatch_barrier_sync ,那么你极可能会使你本身(更确切的说是,你的代码)产生死锁。写操做须要 barrier,而且能够是 async 的。

 

7. 锁竞争

  首先,这里有一个警告:上面这个例子中咱们保护的资源是一个 NSMutableDictionary,出于这样的目的,这段代码运行地至关不错。可是在真实的代码中,把隔离放到正确的复杂度层级下是很重要的。

  若是你对 NSMutableDictionary 的访问操做变得很是频繁,你会碰到一个已知的叫作锁竞争的问题。锁竞争并非只是在 GCD 和队列下才变得特殊,任何使用了锁机制的程序都会碰到一样的问题——只不过不一样的锁机制会以不一样的方式碰到。

  全部对 dispatch_asyncdispatch_sync 等等的调用都须要完成某种形式的锁——以确保仅有一个线程或者特定的线程运行指定的代码。GCD 某些程序上可使用时序(译注:原词为 scheduling)来避免使用锁,但在最后,问题只是稍有变化。根本问题仍然存在:若是你有大量的线程在相同时间去访问同一个锁或者队列,你就会看到性能的变化。性能会严重降低。

  你应该从直接复杂层次中隔离开。当你发现了性能降低,这明显代表代码中存在设计问题。这里有两个开销须要你来平衡。第一个是独占临界区资源过久的开销,以致于别的线程都由于进入临界区的操做而阻塞。第二个是太频繁出入临界区的开销。在 GCD 的世界里,第一种开销的状况就是一个 block 在隔离队列中运行,它可能潜在的阻塞了其余将要在这个隔离队列中运行的代码。第二种开销对应的就是调用 dispatch_asyncdispatch_sync 。不管再怎么优化,这两个操做都不是无代价的。

  使人忧伤的,不存在通用的标准来指导如何正确的平衡,你须要本身评测和调整。启动 Instruments 观察你的 app 忙于什么操做。

  若是你看上面例子中的代码,咱们的临界区代码仅仅作了很简单的事情。这多是也可能不是好的方式,依赖于它怎么被使用。

  在你本身的代码中,要考虑本身是否在更高的层次保护了隔离队列。举个例子,类 Foo 有一个隔离队列而且它自己保护着对NSMutableDictionary 的访问,代替的,能够有一个用到了 Foo 类的 Bar 类有一个隔离队列保护全部对类 Foo 的使用。换句话说,你能够把类 Foo 变为非线程安全的(没有隔离队列),并在 Bar 中,使用一个隔离队列来确保任什么时候刻只能有一个线程使用 Foo

 

8. 全都使用异步分发

  咱们在这稍稍转变如下话题。正如你在上面看到的,你能够同步和异步地分发一个 block,一个工做单元。在 GCD 中,以同步分发的方式很是容易出现这种状况。见下面的代码:

dispatch_queue_t queueA; // assume we have this
dispatch_sync(queueA, ^(){
    dispatch_sync(queueA, ^(){
        foo();
    });
});

  一旦咱们进入到第二个 dispatch_sync 就会发生死锁。咱们不能分发到queueA,由于有人(当前线程)正在队列中而且永远不会离开。可是有更隐晦的产生死锁方式:

dispatch_queue_t queueA; // assume we have this
dispatch_queue_t queueB; // assume we have this

dispatch_sync(queueA, ^(){
    foo();
});

void foo(void)
{
    dispatch_sync(queueB, ^(){
        bar();
    });
}

void bar(void)
{
    dispatch_sync(queueA, ^(){
        baz();
    });
}

  单独的每次调用 dispatch_sync() 看起来都没有问题,可是一旦组合起来,就会发生死锁。

  这是使用同步分发存在的固有问题,若是咱们使用异步分发,好比:

dispatch_queue_t queueA; // assume we have this
dispatch_async(queueA, ^(){
    dispatch_async(queueA, ^(){
        foo();
    });
});

  一切运行正常。异步调用不会产生死锁。所以值得咱们在任何可能的时候都使用异步分发。咱们使用一个异步调用结果 block 的函数,来代替编写一个返回值(必需要用同步)的方法或者函数。这种方式,咱们会有更少发生死锁的可能性。

  异步调用的反作用就是它们很难调试。当咱们在调试器里停止代码运行,回溯并查看已经变得没有意义了。

  要牢记这些。死锁一般是最难处理的问题。

 

9. 如何写出好的异步 API

  若是你正在给设计一个给别人(或者是给本身)使用的 API,你须要记住几种好的实践。

  正如咱们刚刚提到的,你须要倾向于异步 API。当你建立一个 API,它会在你的控制以外以各类方式调用,若是你的代码能产生死锁,那么死锁就会发生。

  若是你须要写的函数或者方法,那么让它们调用 dispatch_async() 。不要让你的函数调用者来这么作,这个调用应该在你的方法或者函数中来作。

  若是你的方法或函数有一个返回值,异步地将其传递给一个回调处理程序。这个 API 应该是这样的,你的方法或函数同时持有一个结果 block 和一个将结果传递过去的队列。你函数的调用者不须要本身来作分发。这么作的缘由很简单:几乎全部时间,函数调用都应该在一个适当的队列中,并且以这种方式编写的代码是很容易阅读的。总之,你的函数将会(必须)调用 dispatch_async() 去运行回调处理程序,因此它同时也可能在须要调用的队列上作这些工做。

  若是你写一个类,让你类的使用者设置一个回调处理队列或许会是一个好的选择。你的代码可能像这样:

- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler;
{
    dispatch_async(self.isolationQueue, ^(void){
        // do actual processing here
        dispatch_async(self.resultQueue, ^(void){
            handler(YES);
        });
    });
}

  若是你以这种方式来写你的类,让类之间协同工做就会变得容易。若是类 A 使用了类 B,它会把本身的隔离队列设置为 B 的回调队列。

 

10. 迭代执行

  若是你正在倒弄一些数字,而且手头上的问题能够拆分出一样性质的部分,那么 dispatch_apply 会颇有用。

  若是你的代码看起来是这样的:

for (size_t y = 0; y < height; ++y) {
    for (size_t x = 0; x < width; ++x) {
        // Do something with x and y here
    }
}

  小小的改动或许就可让它运行的更快:

dispatch_apply(height, dispatch_get_global_queue(0, 0), ^(size_t y) {
    for (size_t x = 0; x < width; x += 2) {
        // Do something with x and y here
    }
});

  代码运行良好的程度取决于你在循环内部作的操做。

  block 中运行的工做必须是很是重要的,不然这个头部信息就显得过于繁重了。除非代码受到计算带宽的约束,每一个工做单元为了很好适应缓存大小而读写的内存都是临界的。这会对性能会带来显著的影响。受到临界区约束的代码可能不会很好地运行。详细讨论这些问题已经超出了这篇文章的范围。使用 dispatch_apply 可能会对性能提高有所帮助,可是性能优化自己就是个很复杂的主题。维基百科上有一篇关于 Memory-bound function 的文章。内存访问速度在 L2,L3 和主存上变化很显著。当你的数据访问模式与缓存大小不匹配时,10倍性能降低的状况并很多见。

 

11. 组

  不少时候,你发现须要将异步的 block 组合起来去完成一个给定的任务。这些任务中甚至有些是并行的。如今,若是你想要在这些任务都执行完成后运行一些代码,"groups" 能够完成这项任务。看这里的例子:

  首先定义group和queue

@property (nonatomic, strong) dispatch_queue_t queue_t_a;
@property (nonatomic, strong) dispatch_group_t group_t;

self.queue_t_a = dispatch_queue_create("qa", 0);
self.group_t = dispatch_group_create();

  而后运行

    dispatch_group_async(self.group_t, _queue_t_a, ^{
        sleep(3);
        NSLog(@"1");
    });
    
    dispatch_group_async(self.group_t, _queue_t_a, ^{
        NSLog(@"2");
    });
    
    dispatch_group_notify(self.group_t, _queue_t_a, ^{
        NSLog(@"3");
    });
    
    NSLog(@"viewDidAppear");

  执行打印顺序永远都是viewDidAppear、一、二、3。注意这里只用到一个queue与group。dispatch_group_notify是等待上面全部queue a执行完以后,再执行。能够看看官网说明

 

12. 对现有API使用 dispatchgroupt

  一旦你将 groups 做为你的工具箱中的一部分,你可能会怀疑为何大多数的异步API不把 dispatch_group_t 做为一个可选参数。这没有什么没法接受的理由,仅仅是由于本身添加这个功能太简单了,可是你仍是要当心以确保本身使用 groups 的代码是成对出现的。

  举例来讲,咱们能够给 Core Data 的 -performBlock: API 函数添加上 groups,就像这样:

- (void)withGroup:(dispatch_group_t)group performBlock:(dispatch_block_t)block
{
    if (group == NULL) {
        [self performBlock:block];
    } else {
        dispatch_group_enter(group);
        [self performBlock:^(){
            block();
            dispatch_group_leave(group);
        }];
    }
}

  当 Core Data 上的一系列操做(极可能和其余的代码组合起来)完成之后,咱们可使用 dispatch_group_notify 来运行一个 block 。

  很明显,咱们能够给 NSURLConnection 作一样的事情:

+ (void)withGroup:(dispatch_group_t)group 
        sendAsynchronousRequest:(NSURLRequest *)request 
        queue:(NSOperationQueue *)queue 
        completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
{
    if (group == NULL) {
        [self sendAsynchronousRequest:request 
                                queue:queue 
                    completionHandler:handler];
    } else {
        dispatch_group_enter(group);
        [self sendAsynchronousRequest:request 
                                queue:queue 
                    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
            handler(response, data, error);
            dispatch_group_leave(group);
        }];
    }
}

为了能正常工做,你须要确保:

  • dispatch_group_enter() 必需要在 dispatch_group_leave()以前运行。
  • dispatch_group_enter()dispatch_group_leave() 一直是成对出现的(就算有错误产生时)。

 

13. 事件源

  GCD 有一个较少人知道的特性:事件源 dispatch_source_t

  跟 GCD 同样,它也是很底层的东西。当你须要用到它时,它会变得极其有用。它的一些使用是秘传招数,咱们将会接触到一部分的使用。可是大部分事件源在 iOS 平台不是颇有用,由于在 iOS 平台有诸多限制,你没法启动进程(所以就没有必要监视进程),也不能在你的 app bundle 以外写数据(所以也就没有必要去监视文件)等等。

  GCD 事件源是以极其资源高效的方式实现的。

 

14. 监视进程

  若是一些进程正在运行而你想知道他们何时存在,GCD 可以作到这些。你也可使用 GCD 来检测进程何时分叉,也就是产生子进程或者传送给了进程的一个信号(好比 SIGTERM)。

NSRunningApplication *mail = [NSRunningApplication 
  runningApplicationsWithBundleIdentifier:@"com.apple.mail"];
if (mail == nil) {
    return;
}
pid_t const pid = mail.processIdentifier;
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, 
  DISPATCH_PROC_EXIT, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(self.source, ^(){
    NSLog(@"Mail quit.");
});
dispatch_resume(self.source);

当 Mail.app 退出的时候,这个程序会打印出 Mail quit.

注意:在全部的事件源被传递到你的事件处理器以前,必须调用 dispatch_resume()

 

15. 监视文件

  这种可能性是无穷的。你能直接监视一个文件的改变,而且当改变发生时事件源的事件处理将会被调用。

  你也可使用它来监视文件夹,好比建立一个 watch folder

NSURL *directoryURL; // assume this is set to a directory
int const fd = open([[directoryURL path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
    char buffer[80];
    strerror_r(errno, buffer, sizeof(buffer));
    NSLog(@"Unable to open \"%@\": %s (%d)", [directoryURL path], buffer, errno);
    return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, 
  DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
    unsigned long const data = dispatch_source_get_data(source);
    if (data & DISPATCH_VNODE_WRITE) {
        NSLog(@"The directory changed.");
    }
    if (data & DISPATCH_VNODE_DELETE) {
        NSLog(@"The directory has been deleted.");
    }
});
dispatch_source_set_cancel_handler(source, ^(){
    close(fd);
});
self.source = source;
dispatch_resume(self.source);

  你应该老是添加 DISPATCH_VNODE_DELETE 去检测文件或者文件夹是否已经被删除——而后就中止监听。

 

16. 定时器

  大多数状况下,对于定时事件你会选择 NSTimer。定时器的GCD版本是底层的,它会给你更多控制权——但要当心使用。

  须要特别重点指出的是,为了让 OS 节省电量,须要为 GCD 的定时器接口指定一个低的余地值(译注:原文leeway value)。若是你没必要要的指定了一个低余地值,将会浪费更多的电量。

  这里咱们设定了一个5秒的定时器,并容许有十分之一秒的余地值:

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 
  0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
    NSLog(@"Time flies.");
});
dispatch_time_t start
dispatch_source_set_timer(source, DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC, 
  100ull * NSEC_PER_MSEC);
self.source = source;
dispatch_resume(self.source);

 

17. 取消

  全部的事件源都容许你添加一个 cancel handler 。这对清理你为事件源建立的任何资源都是颇有帮助的,好比关闭文件描述符。GCD 保证在 cancel handle 调用前,全部的事件处理都已经完成调用。

  参考上面的监视文件例子中对 dispatch_source_set_cancel_handler() 的使用。

 

18. 输入输出

  写出可以在繁重的 I/O 处理状况下运行良好的代码是一件很是棘手的事情。GCD 有一些可以帮上忙的地方。不会涉及太多的细节,咱们只简单的分析下问题是什么,GCD 是怎么处理的。

  习惯上,当你从一个网络套接字中读取数据时,你要么作一个阻塞的读操做,也就是让你个线程一直等待直到数据变得可用,或者是作反复的轮询。这两种方法都是很浪费资源而且没法度量。然而,kqueue 经过当数据变得可用时传递一个事件解决了轮询的问题,GCD 也采用了一样的方法,可是更加优雅。当向套接字写数据时,一样的问题也存在,这时你要么作阻塞的写操做,要么等待套接字直到可以接收数据。

  在处理 I/O 时,还有一个问题就是数据是以数据块的形式到达的。当从网络中读取数据时,依据 MTU([]最大传输单元](https://en.wikipedia.org/wiki/Maximumtransmissionunit)),数据块典型的大小是在1.5K字节左右。这使得数据块内能够是任何内容。一旦数据到达,你一般只是对跨多个数据块的内容感兴趣。并且一般你会在一个大的缓冲区里将数据组合起来而后再进行处理。假设(人为例子)你收到了这样8个数据块:

0: HTTP/1.1 200 OK\r\nDate: Mon, 23 May 2005 22:38
1: :34 GMT\r\nServer: Apache/1.3.3.7 (Unix) (Red-H
2: at/Linux)\r\nLast-Modified: Wed, 08 Jan 2003 23
3: :11:55 GMT\r\nEtag: "3f80f-1b6-3e1cb03b"\r\nCon
4: tent-Type: text/html; charset=UTF-8\r\nContent-
5: Length: 131\r\nConnection: close\r\n\r\n<html>\r
6: \n<head>\r\n  <title>An Example Page</title>\r\n
7: </head>\r\n<body>\r\n  Hello World, this is a ve

  若是你是在寻找 HTTP 的头部,将全部数据块组合成一个大的缓冲区而且从中查找 \r\n\r\n 是很是简单的。可是这样作,你会大量地复制这些数据。大量 旧的 C 语言 API 存在的另外一个问题就是,缓冲区没有全部权的概念,因此函数不得不将数据再次拷贝到本身的缓冲区中——又一次的拷贝。拷贝数据操做看起来是可有可无的,可是当你正在作大量的 I/O 操做的时候,你会在 profiling tool(Instruments) 中看到这些拷贝操做大量出现。即便你仅仅每一个内存区域拷贝一次,你仍是使用了两倍的存储带宽而且占用了两倍的内存缓存。

 

19. GCD 和缓冲区

  最直接了当的方法是使用数据缓冲区。GCD 有一个 dispatch_data_t 类型,在某种程度上和 Objective-C 的 NSData 类型很类似。可是它能作别的事情,并且更通用。

  注意,dispatch_data_t 能够被 retained 和 releaseed ,而且 dispatch_data_t 拥有它持有的对象。

  这看起来可有可无,可是咱们必须记住 GCD 只是纯 C 的 API,而且不能使用Objective-C。一般的作法是建立一个缓冲区,这个缓冲区要么是基于栈的,要么是 malloc 操做分配的内存区域 —— 这些都没有全部权。

  dispatch_data_t 的一个至关独特的属性是它能够基于零碎的内存区域。这解决了咱们刚提到的组合内存的问题。当你要将两个数据对象链接起来时:

dispatch_data_t a; // Assume this hold some valid data
dispatch_data_t b; // Assume this hold some valid data
dispatch_data_t c = dispatch_data_create_concat(a, b);

  数据对象 c 并不会将 a 和 b 拷贝到一个单独的,更大的内存区域里去。相反,它只是简单地 retain 了 a 和 b。你可使用dispatch_data_apply 来遍历对象 c 持有的内存区域:

dispatch_data_apply(c, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
    fprintf(stderr, "region with offset %zu, size %zu\n", offset, size);
    return true;
});

  相似的,你可使用 dispatch_data_create_subrange 来建立一个不作任何拷贝操做的子区域。

 

20. 读和写

  在 GCD 的核内心,调度 I/O(译注:原文为 Dispatch I/O) 与所谓的通道有关。调度 I/O 通道提供了一种与从文件描述符中读写不一样的方式。建立这样一个通道最基本的方式就是调用:

dispatch_io_t dispatch_io_create(dispatch_io_type_t type, dispatch_fd_t fd, 
  dispatch_queue_t queue, void (^cleanup_handler)(int error));

  这将返回一个持有文件描述符的建立好的通道。在你经过它建立了通道以后,你不许以任何方式修改这个文件描述符。

  有两种从根本上不一样类型的通道:流和随机存取。若是你打开了硬盘上的一个文件,你可使用它来建立一个随机存取的通道(由于这样的文件描述符是可寻址的)。若是你打开了一个套接字,你能够建立一个流通道。

  若是你想要为一个文件建立一个通道,你最好使用须要一个路径参数的 dispatch_io_create_with_path ,而且让 GCD 来打开这个文件。这是有益的,由于GCD会延迟打开这个文件以限制相同时间内同时打开的文件数量。

  相似一般的 read(2),write(2) 和 close(2) 的操做,GCD 提供了 dispatch_io_readdispatch_io_writedispatch_io_close。不管什么时候数据读完或者写完,读写操做调用一个回调 block 来结束。这些都是以非阻塞,异步 I/O 的形式高效实现的。

  在这你得不到全部的细节,可是这里会提供一个建立TCP服务端的例子:

  首先咱们建立一个监听套接字,而且设置一个接受链接的事件源:

_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_nativeSocket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in sin = {};
sin.sin_len = sizeof(sin);
sin.sin_family = AF_INET6;
sin.sin_port = htons(port);
sin.sin_addr.s_addr= INADDR_ANY;
int err = bind(result.nativeSocket, (struct sockaddr *) &sin, sizeof(sin));
NSCAssert(0 <= err, @"");

_eventSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _nativeSocket, 0, _isolation);
dispatch_source_set_event_handler(result.eventSource, ^{
    acceptConnection(_nativeSocket);
});

  当接受了链接,咱们建立一个I/O通道:

typedef union socketAddress {
    struct sockaddr sa;
    struct sockaddr_in sin;
    struct sockaddr_in6 sin6;
} socketAddressUnion;

socketAddressUnion rsa; // remote socket address
socklen_t len = sizeof(rsa);
int native = accept(nativeSocket, &rsa.sa, &len);
if (native == -1) {
    // Error. Ignore.
    return nil;
}

_remoteAddress = rsa;
_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_channel = dispatch_io_create(DISPATCH_IO_STREAM, native, _isolation, ^(int error) {
    NSLog(@"An error occured while listening on socket: %d", error);
});

//dispatch_io_set_high_water(_channel, 8 * 1024);
dispatch_io_set_low_water(_channel, 1);
dispatch_io_set_interval(_channel, NSEC_PER_MSEC * 10, DISPATCH_IO_STRICT_INTERVAL);

socketAddressUnion lsa; // remote socket address
socklen_t len = sizeof(rsa);
getsockname(native, &lsa.sa, &len);
_localAddress = lsa;

  若是咱们想要设置 SO_KEEPALIVE(若是使用了HTTP的keep-alive),咱们须要在调用 dispatch_io_create 前这么作。

  建立好 I/O 通道后,咱们能够设置读取处理程序:

dispatch_io_read(_channel, 0, SIZE_MAX, _isolation, ^(bool done, dispatch_data_t data, int error){
    if (data != NULL) {
        if (_data == NULL) {
            _data = data;
        } else {
            _data = dispatch_data_create_concat(_data, data);
        }
        [self processData];
    }
});

  若是全部你想作的只是读取或者写入一个文件,GCD 提供了两个方便的封装: dispatch_readdispatch_write 。你须要传递给dispatch_read 一个文件路径和一个在全部数据块读取后调用的 block。相似的,dispatch_write 须要一个文件路径和一个被写入的 dispatch_data_t 对象。

 

21. 基准测试

  在 GCD 的一个不起眼的角落,你会发现一个适合优化代码的灵巧小工具:

uint64_t dispatch_benchmark(size_t count, void (^block)(void));

  把这个声明放到你的代码中,你就可以测量给定的代码执行的平均的纳秒数。例子以下:

size_t const objectCount = 1000;
uint64_t n = dispatch_benchmark(10000, ^{
    @autoreleasepool {
        id obj = @42;
        NSMutableArray *array = [NSMutableArray array];
        for (size_t i = 0; i < objectCount; ++i) {
            [array addObject:obj];
        }
    }
});
NSLog(@"-[NSMutableArray addObject:] : %llu ns", n);

  在个人机器上输出了:

-[NSMutableArray addObject:] : 31803 ns

  也就是说添加1000个对象到 NSMutableArray 总共消耗了31803纳秒,或者说平均一个对象消耗32纳秒。

  正如 dispatch_benchmark帮助页面指出的,测量性能并不是如看起来那样不重要。尤为是当比较并发代码和非并发代码时,你须要注意特定硬件上运行的特定计算带宽和内存带宽。不一样的机器会很不同。若是代码的性能与访问临界区有关,那么咱们上面提到的锁竞争问题就会有所影响。

  不要把它放到发布代码中,事实上,这是无心义的,它是私有API。它只是在调试和性能分析上起做用。

  访问帮助界面:

curl "http://opensource.apple.com/source/libdispatch/libdispatch-84.5/man/dispatch_benchmark.3?txt" 
  | /usr/bin/groffer --tty -T utf8

 

22. 原子操做

  头文件 libkern/OSAtomic.h 里有许多强大的函数,专门用来底层多线程编程。尽管它是内核头文件的一部分,它也可以在内核以外来帮助编程。

  这些函数都是很底层的,而且你须要知道一些额外的事情。就算你已经这样作了,你还可能会发现一两件你不能作,或者不易作的事情。当你正在为编写高性能代码或者正在实现无锁的和无等待的算法工做时,这些函数会吸引你。

  这些函数在 atomic(3) 的帮助页里所有有概述——运行 man 3 atomic 命令以获得完整的文档。你会发现里面讨论到了内存屏障。查看维基百科中关于内存屏障的文章。若是你还存在疑问,那么你极可能须要它。

 

23. 计数器

  OSAtomicIncrementOSAtomicDecrement 有一个很长的函数列表容许你以原子操做的方式去增长和减小一个整数值 —— 没必要使用锁(或者队列)同时也是线程安全的。若是你须要让一个全局的计数器值增长,而这个计数器为了统计目的而由多个线程操做,使用原子操做是颇有帮助的。若是你要作的仅仅是增长一个全局计数器,那么无屏障版本的 OSAtomicIncrement 是很合适的,而且当没有锁竞争时,调用它们的代价很小。

  相似的,OSAtomicOrOSAtomicAndOSAtomicXor 的函数能用来进行逻辑运算,而 OSAtomicTest 能够用来设置和清除位。

 

24. 比较和交换

OSAtomicCompareAndSwap 能用来作无锁的惰性初始化,以下:

void * sharedBuffer(void)
{
    static void * buffer;
    if (buffer == NULL) {
        void * newBuffer = calloc(1, 1024);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, newBuffer, &buffer)) {
            free(newBuffer);
        }
    }
    return buffer;
}

  若是没有 buffer,咱们会建立一个,而后原子地将其写到 buffer 中若是 buffer 为NULL。在极少的状况下,其余人在当前线程同时设置了 buffer ,咱们简单地将其释放掉。由于比较和交换方法是原子的,因此它是一个线程安全的方式去惰性初始化值。NULL的检测和设置 buffer 都是以原子方式完成的。

  明显的,使用 dispatch_once() 咱们也能够完成相似的事情。

 

25. 原子队列

  OSAtomicEnqueue()OSAtomicDequeue() 可让你以线程安全,无锁的方式实现一个LIFO队列(常见的就是栈)。对有潜在精确要求的代码来讲,这会是强大的代码。

  还有 OSAtomicFifoEnqueue()OSAtomicFifoDequeue() 函数是为了操做FIFO队列,但这些只有在头文件中才有文档 —— 阅读他们的时候要当心。

 

26. 自旋锁

  最后,OSAtomic.h 头文件定义了使用自旋锁的函数:OSSpinLock。一样的,维基百科有深刻的有关自旋锁的信息。使用命令 man 3 spinlock 查看帮助页的 spinlock(3) 。当没有锁竞争时使用自旋锁代价很小。

  在合适的状况下,使用自旋锁对性能优化是颇有帮助的。一如既往:先测量,而后优化。不要作乐观的优化。

  下面是 OSSpinLock 的一个例子:

@interface MyTableViewCell : UITableViewCell

@property (readonly, nonatomic, copy) NSDictionary *amountAttributes;

@end



@implementation MyTableViewCell
{
    NSDictionary *_amountAttributes;
}

- (NSDictionary *)amountAttributes;
{
    if (_amountAttributes == nil) {
        static __weak NSDictionary *cachedAttributes = nil;
        static OSSpinLock lock = OS_SPINLOCK_INIT;
        OSSpinLockLock(&lock);
        _amountAttributes = cachedAttributes;
        if (_amountAttributes == nil) {
            NSMutableDictionary *attributes = [[self subtitleAttributes] mutableCopy];
            attributes[NSFontAttributeName] = [UIFont fontWithName:@"ComicSans" size:36];
            attributes[NSParagraphStyleAttributeName] = [NSParagraphStyle defaultParagraphStyle];
            _amountAttributes = [attributes copy];
            cachedAttributes = _amountAttributes;
        }
        OSSpinLockUnlock(&lock);
    }
    return _amountAttributes;
}

  就上面的例子而言,或许用不着这么麻烦,但它演示了一种理念。咱们使用了ARC的 __weak 来确保一旦 MyTableViewCell 全部的实例都不存在, amountAttributes 会调用 dealloc 。所以在全部的实例中,咱们能够持有字典的一个单独实例。

  这段代码运行良好的缘由是咱们不太可能访问到方法最里面的部分。这是很深奥的——除非你真正须要,否则不要在你的 App 中使用它。

 

能够关注本人的公众号,多年经验的原创文章共享给你们。

相关文章
相关标签/搜索