No.04 Xcode(7.x) GCD相关

阅读前提

易用: GCD比之thread跟简单易用. 由于GCD基于work unit而非像thread那样基于运算, 所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务. 基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文.

性能: GCD自动根据系统负载来增减线程数量, 减少了线程的创建与销毁, 减少了上下文切换, 以及增加了计算效率.

同步: 也称为阻塞. 执行一个同步操作时, 只有等这个操作完成, 之后的代码才可以运行.

异步: 执行一个异步操作时, 不必等待这个操作完成, 后面的代码能够迅速运行.

对象: GCD对象被称为dispatch object, 它包括block, queue, group, source, 等等. 在当前版本Xcode6.2中, dispatch object也是一种Cocoa对象, 他们使用dispatch_create创建, 使用引用计数进行内存管理. GCD对象默认会使用ARC, 所以相关的dispatch_release和dispatch_retain函数被禁止使用. 

dispatch block 任务

block代码块, 本人称之为任务. 实际使用的过程中, 有些pthread的感觉, 它有create/notify/wait/cancel等方法. 直接用等号赋值的方式也能产生block, 但是这种block不属于GCD对象. 只有使用使用dispatch_block_create所生成的block才是GCD对象.

1)调用dispatch_block_create产生block, 至于参数什么意思, 现在还不清楚... 但是推荐使用DISPATCH_BLOCK_DETACHED.

2)生成block后, 把它放入queue中运行, 这个时候可以通过调用dispatch_block_notify来获取完成事件. notify是一个异步方法.

3)当block置于queue后: 
如果还未开始执行, 使用dispatch_block_cancel可以取消这个block; 
如果已经开始执行, 使用dispatch_block_cancel不能取消这个block. 
而无论是否在执行, cancel之后, 再调用dispatch_block_testcancel, 会返回1. 也就是说, testcancel只能表明是否调用过cancel.

4)当block置于queue后, 使用dispatch_block_wait可以等待block执行结束. 它是个同步操作, 返回0代表等到了执行结束. 经过变通后, 这个方法可以用于测试block是否正在执行, 请参考dispatch_group_wait.

示例01代码: 与代码块相关的一些基本操作.

dispatch_queue_t queue = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_SERIAL);

dispatch_block_t block1 = dispatch_block_create(DISPATCH_BLOCK_DETACHED, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"执行任务1... %d", i);
        sleep(2);
    }
});

dispatch_async(queue, block1);

dispatch_block_notify(block1, dispatch_get_main_queue(), ^{
    NSLog(@"执行任务1完毕. 是否取消过: %ld", dispatch_block_testcancel(block1));
});

dispatch_block_t block2 = dispatch_block_create(DISPATCH_BLOCK_DETACHED, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"执行任务2... %d", i);
        sleep(1);
    }
});

dispatch_async(queue, block2);

dispatch_block_notify(block2, dispatch_get_main_queue(), ^{
    NSLog(@"执行任务2完毕. 是否取消过: %ld", dispatch_block_testcancel(block2));
});

sleep(1);

dispatch_block_cancel(block1);
dispatch_block_cancel(block2);

NSLog(@"是否取消过: block1 = %ld, block2 = %ld", dispatch_block_testcancel(block1), dispatch_block_testcancel(block2));
    

运行01结果: 由于queue是一个串行队列, 在cancel的时候, block2是还没有开始执行的, 所以block2能够被取消.

2015-08-18 16:11:04.512 asasasas[6357:106160] 执行任务1... 0
2015-08-18 16:11:05.513 asasasas[6357:106088] 是否取消过: block1 = 1, block2 = 1
2015-08-18 16:11:06.516 asasasas[6357:106160] 执行任务1... 1
2015-08-18 16:11:08.519 asasasas[6357:106160] 执行任务1... 2
2015-08-18 16:11:10.524 asasasas[6357:106088] 执行任务1完毕. 是否取消过: 1
2015-08-18 16:11:10.524 asasasas[6357:106088] 执行任务2完毕. 是否取消过: 1
    

dispatch queue 队列

列队可以接受任务, 并将任务以先到先执行的顺序来执行. dispatch queue可以是并发的或串行的. 并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行, 串行队列同一时间只执行单一任务.

调用dispatch_queue_create可以创建一个队列. 函数的第一个参数是一个标签, 这纯是为了debug. Apple建议我们使用倒置域名来命名队列, 比如“com.dreamingwish.task”. 这些名字会在崩溃日志中被显示出来, 也可以被调试器调用, 这在调试中会很有用. 第二个参数有两个可选值DISPATCH_QUEUE_SERIAL(串行)和DISPATCH_QUEUE_CONCURRENT(并行), 也就是说, 队列有两种类型: 串行队列和并行队列, 区别是队列中所有的块的执行是同步还是异步
可以简单的认为, 一个串行列队只有一个线程, 所有的块都在这个线程上运行, 一个并行列队会有很多线程, 有多少个块就有多少个线程.

串行队列: 当多个block被提交到queue上时, block会按照提交的时间顺序依次执行.

并行队列: 当多个block被提交到queue上时, block会在提交的时候就开始执行, block之间并无先后关系. 具体开始执行的时间, 与系统调度有关. 由于并行队列的特性, 并行队列可以直接选用全局队列, 而不需要重新创建.

系统中存在着两个预置的队列: 主要队列和全局队列.

主要队列: 串行队列, 这是因为提交至主要队列的任务, 只会在主线程中执行. 主要队列可以调用dispatch_get_main_queue()来获得. 需要注意的是: 不要在主线程中, 同步提交任务到主要队列, 否则会造成死锁.

全局队列: 并行队列, 由整个进程共享. 进程中存在三个优先级不同的全局队列:高、中(默认)、低. 可以调用dispatch_get_global_queue, 参数传入优先级来访问对应的全局队列.

调用dispatch_async(异步提交)/dispatch_sync(同步提交)可以向队列提交任务. 提交操作本身是在当前线程运行, 两个参数对应相关的队列和任务. 区别是提交操作本身是同步还是异步

异步提交: 提交操作本身不会阻塞当前线程.

同步提交: 提交操作本身会阻塞当前线程.

示例01代码: 创建一个串行队列, 同步提交几个任务.

dispatch_queue_t queue = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_SERIAL);

NSLog(@"提交第一个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {   
        NSLog(@"完成第一个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第二个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第二个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第三个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第三个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"已经提交完所有任务");
    

运行01结果: 任务被提交时阻塞当前线程, 所有日志被依次输出, 最后一条日志结束后, 当前线程继续执行.

2015-08-17 11:44:08.930 asasasas[1421:49775] 提交第一个任务
2015-08-17 11:44:08.930 asasasas[1421:49775] 完成第一个任务... 0
2015-08-17 11:44:09.931 asasasas[1421:49775] 完成第一个任务... 1
2015-08-17 11:44:10.933 asasasas[1421:49775] 完成第一个任务... 2
2015-08-17 11:44:11.934 asasasas[1421:49775] 提交第二个任务
2015-08-17 11:44:11.934 asasasas[1421:49775] 完成第二个任务... 0
2015-08-17 11:44:12.936 asasasas[1421:49775] 完成第二个任务... 1
2015-08-17 11:44:13.937 asasasas[1421:49775] 完成第二个任务... 2
2015-08-17 11:44:14.938 asasasas[1421:49775] 提交第三个任务
2015-08-17 11:44:14.939 asasasas[1421:49775] 完成第三个任务... 0
2015-08-17 11:44:15.940 asasasas[1421:49775] 完成第三个任务... 1
2015-08-17 11:44:16.942 asasasas[1421:49775] 完成第三个任务... 2
2015-08-17 11:44:17.943 asasasas[1421:49775] 已经提交完所有任务
    

示例02代码: 创建一个串行队列, 异步提交几个任务.

dispatch_queue_t queue = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_SERIAL);

NSLog(@"提交第一个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第一个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第二个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第二个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第三个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第三个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"已经提交完所有任务");
    

运行02结果: 任务在提交时没有阻塞当前线程, 三个完成任务的日志按照加入的顺序依次输出.

2015-08-17 11:41:46.333 asasasas[1399:48958] 提交第一个任务
2015-08-17 11:41:46.333 asasasas[1399:48958] 提交第二个任务
2015-08-17 11:41:46.333 asasasas[1399:48985] 完成第一个任务... 0
2015-08-17 11:41:46.333 asasasas[1399:48958] 提交第三个任务
2015-08-17 11:41:46.334 asasasas[1399:48958] 已经提交完所有任务
2015-08-17 11:41:47.339 asasasas[1399:48985] 完成第一个任务... 1
2015-08-17 11:41:48.344 asasasas[1399:48985] 完成第一个任务... 2
2015-08-17 11:41:49.348 asasasas[1399:48985] 完成第二个任务... 0
2015-08-17 11:41:50.353 asasasas[1399:48985] 完成第二个任务... 1
2015-08-17 11:41:51.355 asasasas[1399:48985] 完成第二个任务... 2
2015-08-17 11:41:52.357 asasasas[1399:48985] 完成第三个任务... 0
2015-08-17 11:41:53.359 asasasas[1399:48985] 完成第三个任务... 1
2015-08-17 11:41:54.364 asasasas[1399:48985] 完成第三个任务... 2
    

示例03代码: 创建一个并行队列, 同步提交几个任务.

dispatch_queue_t queue = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_CONCURRENT);

NSLog(@"提交第一个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第一个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第二个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第二个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第三个任务");
dispatch_sync(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第三个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"已经提交完所有任务");
    

运行03结果: 任务被提交时阻塞当前线程, 所有日志被依次输出, 最后一条日志结束后, 当前线程继续执行. 注意: 本次测试和示例01的结果完全相同. 这是因为任务一、二、三是在同一个线程中被提交, 提交操作本身被阻塞导致下个任务无法提交. 如果提交操作放到不同的线程中, 结果会与示例01不同.

2015-08-17 11:41:02.664 asasasas[1378:48460] 提交第一个任务
2015-08-17 11:41:02.665 asasasas[1378:48460] 完成第一个任务... 0
2015-08-17 11:41:03.666 asasasas[1378:48460] 完成第一个任务... 1
2015-08-17 11:41:04.668 asasasas[1378:48460] 完成第一个任务... 2
2015-08-17 11:41:05.669 asasasas[1378:48460] 提交第二个任务
2015-08-17 11:41:05.669 asasasas[1378:48460] 完成第二个任务... 0
2015-08-17 11:41:06.671 asasasas[1378:48460] 完成第二个任务... 1
2015-08-17 11:41:07.672 asasasas[1378:48460] 完成第二个任务... 2
2015-08-17 11:41:08.674 asasasas[1378:48460] 提交第三个任务
2015-08-17 11:41:08.674 asasasas[1378:48460] 完成第三个任务... 0
2015-08-17 11:41:09.676 asasasas[1378:48460] 完成第三个任务... 1
2015-08-17 11:41:10.677 asasasas[1378:48460] 完成第三个任务... 2
2015-08-17 11:41:11.678 asasasas[1378:48460] 已经提交完所有任务
    

示例04代码: 创建一个并行队列, 异步提交几个任务.

dispatch_queue_t queue = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_CONCURRENT);

NSLog(@"提交第一个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第一个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第二个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第二个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"提交第三个任务");
dispatch_async(queue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"完成第三个任务... %d", i);
        sleep(1);
    }
});

NSLog(@"已经提交完所有任务");
    

运行04结果: 任务在提交时没有阻塞当前线程, 三个完成任务的日志输入没有先后顺序.

2015-08-17 12:04:21.543 asasasas[1474:55827] 提交第一个任务
2015-08-17 12:04:21.544 asasasas[1474:55827] 提交第二个任务
2015-08-17 12:04:21.544 asasasas[1474:55858] 完成第一个任务... 0
2015-08-17 12:04:21.544 asasasas[1474:55827] 提交第三个任务
2015-08-17 12:04:21.544 asasasas[1474:55859] 完成第二个任务... 0
2015-08-17 12:04:21.544 asasasas[1474:55827] 已经提交完所有任务
2015-08-17 12:04:21.545 asasasas[1474:55856] 完成第三个任务... 0
2015-08-17 12:04:22.548 asasasas[1474:55856] 完成第三个任务... 1
2015-08-17 12:04:22.548 asasasas[1474:55858] 完成第一个任务... 1
2015-08-17 12:04:22.548 asasasas[1474:55859] 完成第二个任务... 1
2015-08-17 12:04:23.550 asasasas[1474:55856] 完成第三个任务... 2
2015-08-17 12:04:23.550 asasasas[1474:55859] 完成第二个任务... 2
2015-08-17 12:04:23.550 asasasas[1474:55858] 完成第一个任务... 2
    

dispatch group 队列组

队列组是用于管理队列的GCD对象. 一个队列组可以用来将多个block组成一组以监测这些Block全部完成或者等待全部完成时发出的消息. 

1. 调用dispatch_group_create来创建以一个队列组.

2. 调用dispatch_group_async/dispatch_group_sync来将block提交至一个queue, 同时将它们添加至group. 可以认为是: dispatch_async/dispatch_sync操作, 额外增加一个group参数, 用以标记同步提交/异步提交的操作被加入了一个队列组中. 

3. 调用dispatch_group_notify来添加一个监听, 用于监听列队组中所有的块都已经执行结束, 当队列组执行结束后, 在指定的队列上执行块. 操作本身是一个异步行为.

4. 调用dispatch_group_wait来等待列队组中所有的块都已经执行结束. 操作本身是一个同步行为. 第二个参数能指定等待的时间. 如果所有的块都已经执行结束, 这个操作会返回0, 否则返回一个错误码. 特别提醒: 将等待时间设置到极短, 可以利用此操作判断列队组是否执行结束.

示例01代码: 创建一个队列组, 将三个不同的任务异步提交到对应的队列中, 同时添加到队列组.

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue1 = dispatch_queue_create("com.weizhen.sample", DISPATCH_QUEUE_SERIAL);

NSLog(@"提交第一个任务");
dispatch_group_async(group, queue1, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"执行第一个任务... %d", i);
        sleep(1);
    }
});

dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

NSLog(@"提交第二个任务");
dispatch_group_async(group, queue2, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"执行第二个任务... %d", i);
        sleep(2);
    }
});

dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSLog(@"提交第三个任务");
dispatch_group_async(group, queue3, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"执行第三个任务... %d", i);
        sleep(3);
    }
});

NSLog(@"已经提交完所有任务");

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"任务全部完成!");
});

NSLog(@"所有的结束通知已经设定");

dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 3 * 1000000 * NSEC_PER_USEC);

long result = dispatch_group_wait(group, timeout);

if (result == 0) {
    NSLog(@"结束等待, 所有任务已经完成!");
}
else {
    NSLog(@"结束等待, 等待超时!");
}
    

运行01结果: 列队组中所有的任务完成, 会发出完成通知. "结束通知设定"与"结束等待"之间相差3秒, 这说明等待操作是阻塞的. 如果将超时时间设置较长, 就能等到成功结束的时候.

2015-08-17 15:21:55.567 asasasas[1786:95982] 提交第一个任务
2015-08-17 15:21:55.567 asasasas[1786:95982] 提交第二个任务
2015-08-17 15:21:55.567 asasasas[1786:96006] 执行第一个任务... 0
2015-08-17 15:21:55.568 asasasas[1786:95982] 提交第三个任务
2015-08-17 15:21:55.568 asasasas[1786:95982] 已经提交完所有任务
2015-08-17 15:21:55.568 asasasas[1786:96005] 执行第三个任务... 0
2015-08-17 15:21:55.568 asasasas[1786:95982] 所有的结束通知已经设定
2015-08-17 15:21:55.568 asasasas[1786:96004] 执行第二个任务... 0
2015-08-17 15:21:56.573 asasasas[1786:96006] 执行第一个任务... 1
2015-08-17 15:21:57.578 asasasas[1786:96006] 执行第一个任务... 2
2015-08-17 15:21:57.630 asasasas[1786:96004] 执行第二个任务... 1
2015-08-17 15:21:58.570 asasasas[1786:95982] 结束等待, 等待超时!
2015-08-17 15:21:58.570 asasasas[1786:96005] 执行第三个任务... 1
2015-08-17 15:21:59.702 asasasas[1786:96004] 执行第二个任务... 2
2015-08-17 15:22:01.572 asasasas[1786:96005] 执行第三个任务... 2
2015-08-17 15:22:04.578 asasasas[1786:96004] 任务全部完成!
    

dispatch source

何为Dispatch Sources? 简单来说, dispatch source 是一个监视某些类型事件的对象. 当这些事件发生时, 它自动将一个block放入一个dispatch queue的执行例程中. 说的貌似有点不清不楚. 我们到底讨论哪些事件类型?

下面是GCD 10.6.0版本支持的事件:

  1. Mach port send right state changes.
  2. Mach port receive right state changes.
  3. External process state change.
  4. File descriptor ready for read.
  5. File descriptor ready for write.
  6. Filesystem node event.
  7. POSIX signal.
  8. Custom timer.
  9. Custom event.

这是一堆很有用的东西, 它支持所有kqueue所支持的事件(kqueue是什么?见http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什么?见http://en.wikipedia.org/wiki/Mach_(kernel))端口、内建计时器支持(这样我们就不用使用超时参数来创建自己的计时器)和用户事件.

用户事件. 这些事件里面多数都可以从名字中看出含义, 但是你可能想知道啥叫用户事件. 简单地说, 这种事件是由你调用dispatch_source_merge_data函数来向自己发出的信号. 这个名字对于一个发出事件信号的函数来说, 太怪异了. 这个名字的来由是GCD会在事件句柄被执行之前自动将多个事件进行联结. 你可以将数据“拼接” 至dispatch source中任意次, 并且如果dispatch queue在这期间繁忙的话, GCD只会调用该句柄一次(不要觉得这样会有问题, 看完下面的内容你就明白了).

用户事件有两种: DISPATCH_SOURCE_TYPE_DATA_ADD 和 DISPATCH_SOURCE_TYPE_DATA_OR.用户事件源有个 unsigned long data属性, 我们将一个 unsigned long传入 dispatch_source_merge_data. 当使用 _ADD版本时, 事件在联结时会把这些数字相加. 当使用 _OR版本时, 事件在联结时会把这些数字逻辑与运算. 当事件句柄执行时, 我们可以使用dispatch_source_get_data函数访问当前值, 然后这个值会被重置为0.

让我假设一种情况. 假设一些异步执行的代码会更新一个进度条. 因为主线程只不过是GCD的另一个dispatch queue而已, 所以我们可以将GUI更新工作push到主线程中. 然而, 这些事件可能会有一大堆, 我们不想对GUI进行频繁而累赘的更新, 理想的情况是 当主线程繁忙时将所有的改变联结起来. 用dispatch source就完美了, 使用DISPATCH_SOURCE_TYPE_DATA_ADD, 我们可以将工作拼接起来, 然后主线程可以知道从上一次处理完事件到现在一共发生了多少改变, 然后将这一整段改变一次更新至进度条. 啥也不说了, 上代码:

dispatch_queue_t queue = dispatch_get_main_queue();

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, queue);

dispatch_source_set_event_handler(source, ^{

    [progressIndicator incrementBy:dispatch_source_get_data(source)];
});

dispatch_resume(source);

dispatch_apply([array count], globalQueue, ^(size_t index) {
    // do some work on data at index
    dispatch_source_merge_data(source, 1);
});
    

(对于这段代码, 我很想说点什么, 我第一次用dispatch source时, 我纠结了很久, 很是崩溃:Dispatch queue启动时默认状态是挂起的, 我们创建完毕之后得主动恢复, 否则事件不会被传送)

假设你已经将进度条的min/max值设置好了, 那么这段代码就完美了. 数据会被并发处理. 当每一段数据完成后, 会通知dispatch source并将dispatch source data加1, 这样我们就认为一个单元的工作完成了. 事件句柄根据已完成的工作单元来更新进度条. 若主线程比较空闲并且这些工作单元进行的比较慢, 那么事 件句柄会在每个工作单元完成的时候被调用, 实时更新. 如果主线程忙于其他工作, 或者工作单元完成速度很快, 那么完成事件会被联结起来, 导致进度条只在主线程变得可用时才被更新, 并且一次将积累的改变更新至GUI.

现在你可能会想, 听起来倒是不错, 但是要是我不想让事件被联结呢?有时候你可能想让每一次信号都会引起响应, 什么后台的智能玩意儿统统不要. 啊. . 其实很简单的, 把你的思想放到禁锢的框子之外就行了. 如果你想让每一个信号都得到响应, 那使用dispatch_async函数不就行了. 实际上, 使用的 dispatch source而不使用dispatch_async的唯一原因就是利用联结的优势.

内建事件. 上面就是怎样使用用户事件, 那么内建事件呢?看看下面这个例子, 用GCD读取标准输入:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, STDIN_FILENO, 0, queue);

dispatch_source_set_event_handler(source, ^{

    char buf[1024];

    int len = read(STDIN_FILENO, buf, sizeof(buf));

    if(len > 0) {
        NSLog(@"Got data from stdin: %.*s", len, buf);
    }
});

dispatch_resume(source);
    

简单的要死!因为我们使用的是全局队列, 句柄自动在后台执行, 与程序的其他部分并行, 这意味着对这种情况的提速:事件进入程序时, 程序正在处理其他事务.

这是标准的UNIX方式来处理事务的好处, 不用去写loop. 如果使用经典的 read调用, 我们还得万分留神, 因为返回的数据可能比请求的少, 还得忍受无厘头的“errors”, 比如 EINTR (终端系统调用). 使用GCD, 我们啥都不用管, 就从这些蛋疼的情况里解脱了. 如果我们在文件描述符中留下了未读取的数据, GCD会再次调用我们的句柄.

对于标准输入, 这没什么问题, 但是对于其他文件描述符, 我们必须考虑在完成读写之后怎样清除描述符. 对于dispatch source还处于活跃状态时, 我们决不能关闭描述符. 如果另一个文件描述符被创建了(可能是另一个线程创建的)并且新的描述符刚好被分配了相同的数字, 那么你的dispatch source可能会在不应该的时候突然进入读写状态. de这个bug可不是什么好玩的事儿.

适当的清除方式是使用 dispatch_source_set_cancel_handler, 并传入一个block来关闭文件描述符. 然后我们使用 dispatch_source_cancel来取消dispatch source, 使得句柄被调用, 然后文件描述符被关闭.

使用其他dispatch source类型也差不多. 总的来说, 你提供一个source(mach port、文件描述符、进程ID等等)的区分符来作为diapatch source的句柄. mask参数通常不会被使用, 但是对于 DISPATCH_SOURCE_TYPE_PROC 来说mask指的是我们想要接受哪一种进程事件. 然后我们提供一个句柄, 然后恢复这个source(前面我加粗字体所说的, 得先恢复), 搞定. dispatch source也提供一个特定于source的data, 我们使用 dispatch_source_get_data函数来访问它. 例如, 文件描述符会给出大致可用的字节数. 进程source会给出上次调用之后发生的事件的mask. 具体每种source给出的data的含义, 看man page吧.

计时器

计时器事件稍有不同. 它们不使用handle/mask参数, 计时器事件使用另外一个函数 dispatch_source_set_timer 来配置计时器. 这个函数使用三个参数来控制计时器触发:

start参数控制计时器第一次触发的时刻. 参数类型是 dispatch_time_t, 这是一个opaque类型, 我们不能直接操作它. 我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们. 另外, 常量 DISPATCH_TIME_NOW 和DISPATCH_TIME_FOREVER 通常很有用.

interval参数没什么好解释的.

leeway参数比较有意思. 这个参数告诉系统我们需要计时器触发的精准程度. 所有的计时器都不会保证100%精准, 这个参数用来告诉系统你希望系统保证精准的努力程度. 如果你希望一个计时器没五秒触发一次, 并且越准越好, 那么你传递0为参数. 另外, 如果是一个周期性任 务, 比如检查email, 那么你会希望每十分钟检查一次, 但是不用那么精准. 所以你可以传入60, 告诉系统60秒的误差是可接受的.

这样有什么意义呢?简单来说, 就是降低资源消耗. 如果系统可以让cpu休息足够长的时间, 并在每次醒来的时候执行一个任务集合, 而不是不断的醒来睡 去以执行任务, 那么系统会更高效. 如果传入一个比较大的leeway给你的计时器, 意味着你允许系统拖延你的计时器来将计时器任务与其他任务联合起来一起 执行.

总结: 现在你知道怎样使用GCD的dispatch source功能来监视文件描述符、计时器、联结的用户事件以及其他类似的行为. 由于dispatch source完全与dispatch queue相集成, 所以你可以使用任意的dispatch queue. 你可以将一个dispatch source的句柄在主线程中执行、在全局队列中并发执行、或者在用户队列中串行执行(执行时会将程序的其他模块的运算考虑在内).

GCD介绍(四): 完结

Dispatch Queue挂起

dispatch queue可以被挂起和恢复. 使用 dispatch_suspend函数来挂起, 使用 dispatch_resume 函数来恢复. 这两个函数的行为是如你所愿的. 另外, 这两个还是也可以用于dispatch source.

一个要注意的地方是, dispatch queue的挂起是block粒度的. 换句话说, 挂起一个queue并不会将当前正在执行的block挂起. 它会允许当前执行的block执行完毕, 然后后续的block不再会被执行, 直至queue被恢复.

还有一个注意点:从man页上得来的:如果你挂起了一个queue或者source, 那么销毁它之前, 必须先对其进行恢复.

Dispatch Queue目标指定

所有的用户队列都有一个目标队列概念. 从本质上讲, 一个用户队列实际上是不执行任何任务的, 但是它会将任务传递给它的目标队列来执行. 通常, 目标队列是默认优先级的全局队列.

用户队列的目标队列可以用函数 dispatch_set_target_queue来修改. 我们可以将任意 dispatch queue传递给这个函数, 甚至可以是另一个用户队列, 只要别构成循环就行. 这个函数可以用来设定用户队列的优先级. 比如我们可以将用户队列的目标队列设 定为低优先级的全局队列, 那么我们的用户队列中的任务都会以低优先级执行. 高优先级也是一样道理.

有一个用途, 是将用户队列的目标定为main queue. 这会导致所有提交到该用户队列的block在主线程中执行. 这样做来替代直接在主线程中执行代码的好处在于, 我们的用户队列可以单独地被挂起 和恢复, 还可以被重定目标至一个全局队列, 然后所有的block会变成在全局队列上执行(只要你确保你的代码离开主线程不会有问题).

还有一个用途, 是将一个用户队列的目标队列指定为另一个用户队列. 这样做可以强制多个队列相互协调地串行执行, 这样足以构建一组队列, 通过挂起和暂 停那个目标队列, 我们可以挂起和暂停整个组. 想象这样一个程序:它扫描一组目录并且加载目录中的内容. 为了避免磁盘竞争, 我们要确定在同一个物理磁盘上同 时只有一个文件加载任务在执行. 而希望可以同时从不同的物理磁盘上读取多个文件. 要实现这个, 我们要做的就是创建一个dispatch queue结构, 该结构为磁盘结构的镜像.

首先, 我们会扫描系统并找到各个磁盘, 为每个磁盘创建一个用户队列. 然后扫描文件系统, 并为每个文件系统创建一个用户队列, 将这些用户队列的目标队 列指向合适的磁盘用户队列. 最后, 每个目录扫描器有自己的队列, 其目标队列指向目录所在的文件系统的队列. 目录扫描器枚举自己的目录并为每个文件向自己的 队列提交一个block. 由于整个系统的建立方式, 就使得每个物理磁盘被串行访问, 而多个物理磁盘被并行访问. 除了队列初始化过程, 我们根本不需要手动干预什么东西.

信号量. 

dispatch的信号量是像其他的信号量一样的, 如果你熟悉其他多线程系统中的信号量, 那么这一节的东西再好理解不过了.

信号量是一个整形值并且具有一个初始计数值, 并且支持两个操作:信号通知和等待. 当一个信号量被信号通知, 其计数会被增加. 当一个线程在一个信号量上等待时, 线程会被阻塞(如果有必要的话), 直至计数器大于零, 然后线程会减少这个计数.

我们使用函数 dispatch_semaphore_create 来创建dispatch信号量, 使用函数 dispatch_semaphore_signal 来信号通知, 使用函数 dispatch_semaphore_wait 来等待. 这些函数的man页有两个很好的例子, 展示了怎样使用信号量来同步任务和有限资源访问控制.

单次初始化

GCD还提供单词初始化支持, 这个与pthread中的函数 pthread_once 很相似. GCD提供的方式的优点在于它使用block而非函数指针, 这就允许更自然的代码方式:

这个特性的主要用途是惰性单例初始化或者其他的线程安全数据共享. 典型的单例初始化技术看起来像这样(线程安全的):

+ (id)sharedWhatever
{
    static Whatever *whatever = nil;

    @synchronized([Whatever class]) {

        if(!whatever) {

            whatever = [[Whatever alloc] init];
        }
    }

    return whatever;
}
    

这挺好的, 但是代价比较昂贵;每次调用 +sharedWhatever 函数都会付出取锁的代价, 即使这个锁只需要进行一次. 确实有更风骚的方式来实现这个, 使用类似双向锁或者是原子操作的东西, 但是这样挺难弄而且容易出错.

使用GCD, 我们可以这样重写上面的方法, 使用函数 dispatch_once:

+ (id)sharedWhatever
{
    static dispatch_once_t pred;

    static Whatever *whatever = nil;

    dispatch_once(&pred, ^{

        whatever = [[Whatever alloc] init];
    });

    return whatever;
}
    

这个稍微比 @synchronized方法简单些, 并且GCD确保以更快的方式完成这些检测, 它保证block中的代码在任何线程通过 dispatch_once 调用之前被执行, 但它不会强制每次调用这个函数都让代码进行同步控制. 实际上, 如果你去看这个函数所在的头文件, 你会发现目前它的实现其实是一个宏, 进行了内联的初始化测试, 这意味着通常情况下, 你不用付出函数调用的负载代价, 并且会有更少的同步控制负载.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值