Grand Central Dispatch

这是 Objective-C 高级编程 第三章,终于在拖了快一个月把这本书完结了。最近都比较忙啊,有时间的时候都去熟悉公司的代码了,其他学习的时间就减少了。开始正题吧,GCD 是异步执行任务的技术之一,开发者使用 GCD 来开发多线程程序。首先回顾下苹果官方对 GCD 的说明:开发者要做的只是定义想执行的任务并将其追加到适当的 Dispatch Queue 中。

书中对线程有一个定义是:线程是 1 个 CPU 执行的 CPU 命令列为一条无分叉路径。现在一个物理的 CPU 芯片实际上有 64 个(64 核)CPU,如果一个 CPU 核虚拟为两个 CPU 工作,那么一台计算机上使用多个 CPU 核就是理所当然的事了。尽管如此,“一个 CPU 核执行的 CPU 命令列为一条无分叉路径” 仍然不变。

为什么无论是桌面应用、Android、还是 iOS,开发一个 App 都需要如此重视多线程呢?因为使用多线程编程可以保证应用程序的响应性能。App 都有一个用来描绘用户界面,处理触摸屏幕的事件的线程称为 主线程。如果阻塞了 主线程 的执行,就会导致不能更新用户界面、应用程序的画面长时间停滞等问题,用户就会以为 死机 了。 使用多线程编程,在执行长时间的处理时仍能保证用户界面的响应性能。

总结

GCD 中 Block 是同步执行还是异步执行取决于 Block 所属的 Dispatch QueueSerial 还是 Concurrent。当前线程是否被阻塞是由调用的函数名决定的,比如:dispatch_async 函数不会阻塞当前线程,而 dispatch_sync, dispatch_group_wait 则会阻塞当前线程。

Dispatch Queue

最简单的 GCD 代码格式为:

dispatch_async(queue, ^{
    // 想要执行的任务
});

仅这样就可使指定的 Block 在另一线程中执行。

Dispatch Queue 是一个执行处理的 先入先出(FIFO) 等待队列。开发者通过 dispatch_async 函数等 API,将指定的 Block 封装后追加到指定的 Dispatch Queue 中。

Serial Dispatch Queue 和 Concurrent Dispatch Queue

Dispatch Queue 的种类 说明
Serial Dispatch Queue 等待正在执行中的处理结束(同步执行)
Concurrent Dispatch Queue 不等待正在执行中的处理结束(异步执行)

无论是 Serial 还是 ConcurrentDispatch Queue,它们都是 FIFO 的队列,所以先加入队列的处理一定会先出队,出队后能够占领一个线程,但它占领的线程不一定会先执行。

dispatch_queue_create

// 创建一个 Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
// 创建一个 Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("yogy.jianshu.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create 函数的第一个参数是 Dispatch Queue 的名称,推荐使用逆序全称域名(FQDN),该名称会出现在在应用程序崩溃时的 CrashLog 中。

虽然一个 Serial Dispatch Queue 中的处理是串行执行的,但多个 Serial Dispatch Queue 之间是并行执行的。一旦生成 Serial Dispatch Queue 并追加处理,系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程。如果生成 2000 个 Serial Dispatch Queue,那么就生成了 2000 个线程。而对于 Concurrent Dispatch Queue 来说,不管生成多少,由于 XNU 只使用有效管理的线程,因此不会发生 Serial Dispatch Queue 那样的问题。虽然 Serial Dispatch QueueConcurrent Dispatch Queue 能生成更多的线程,但绝不能激动之下大量生成 Serial Dispatch Queue

dispatch_release

在 ARC 情况下,已经不用它了。

所有通过类似 dispatch_xxxxx_create 函数创建的 GCD 对象,都需要通过 dispatch_release 函数释放。

dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
dispatch_async(serialQueue, ^{
    NSLog(@"www.jianshu.yogy");
});
dispatch_release(serialQueue);

因为每一个加入 Dispatch Queue 中的 Block 都会被封装且持有其加入的 Dispatch Queue 对象,所以,当含有 create 的 API 生成的对象不需要的时候有必要通过 dispatch_release 函数进行释放。在通过函数或方法获取 Dispatch Queue 以及其他名称中含有 create 的 API 生成的对象时,有必要通过 dispatch_retain 函数持有,并在不需要的时候通过 dispatch_release 释放。

Main Dispatch Queue/Global Dispatch Queue

开发者除了使用 dispatch_queue_create 生成 Dispatch Queue 外,还能获取系统标准提供的 Dispatch Queue

Main Dispatch Queue 是 App 的主线程,它是一个 Serial Dispatch Queue。追加到 Main Dispatch Queue 的处理在主线程的 RunLoop 中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到 Main Dispatch Queue 中。

Global Dispatch Queue 是所有应用程序都能够使用的 Concurrent Dispatch QueueGlobal Dispatch Queue 有四种优先级,但是通过 XNU 内核用于 Global Dispatch Queue 的线程并不能保证实时性,因此执行优先级只是大致的判断。例如在处理内容可有可无时,使用后台优先级的 Global Dispatch Queue 等,只能进行这种程度的区分。

// 获取 Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 获取高优先级的 Global Dispatch Queue
dispatch_queue_t globalQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 获取其他优先级的 Global Dispatch Queue,只修改第一个参数即可。优先级有:HIGH, DEFAULT, LOW, BACKGROUND

对于 Main Dispatch QueueGlobal Dispatch Queue 执行 dispatch_retaindispatch_release 函数不会引发任何问题。

使用 Main Dispatch QueueGlobal Dispatch Queue 的例子:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 这里进行可并行执行的处理、比较耗时的处理、占用资源的处理
    dispatch_async(dispatch_get_main_queue(), ^{
        // 这里进行必须在主线程执行的处理,如更新 UI
    });
});

dispatch_set_target_queue

通过 dispatch_queue_create 函数生成的 Dispatch Queue 不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级的 Global Dispatch Queue 相同执行优先级的线程。而变更生成的 Dispatch Queue 的执行优先级要使用 dispatch_set_target_queue 函数。如以下代码:

dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
dispatch_queue_t globalQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(serialQueue, globalQueueBackground);
// 这个函数可以理解为:将 serialQueue 中的处理取出来再加入到 globalQueueBackground 中。

dispatch_set_target_queue 函数的第一个参数不可为:Main Dispatch QueueGlobal Dispatch Queue,否则属于未定义行为。当在必须不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,使用 dispatch_set_target_queue 函数将多个 Serial Dispatch Queue 的目标指定为同一个 Serial Dispatch Queue,即可防止处理并行执行。

dispatch_after

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"www.jianshu.yogy");
});

3ull * NSEC_PER_SEC 指 3s,150ull *NSEC_PER_MSEC 指 150ms,都是按 PER 后面的单词为单位。

该代码并不是指 Block 将在 3s 后被执行,而是指 3s 后才会把 Block 追加到 Main Dispatch Queue 中(类似于 dispatch_async 函数)。所以如果 RunLoop 每隔 1/60s 执行一次,Block 最快在 3s 后执行,最慢在 (3+1/60)s 执行。

NSDate 得到 dispatch_time_t:

dispatch_time_t getDispatchTimeByDate(NSDate date) {
    NSTimeInterval interval;
    double second, subsecond;
    struct timespec time;
    dispatch_time_t milestone;
    
    interval = [date timeIntervalSince1970];
    subsecond = modf(interval, &second);
    time.tv_sec = second;
    time.tv_nsec = subsecond * NSEC_PER_SEC;
    milestone = dispatch_walltime(&time, 0);
    
    return milestone;
}

Dispatch Group

当需要在某一组的多个处理全部执行完成后再执行其他处理,使用 Dispatch GroupDispatch Group 是针对 Block 而言的,并不是针对 Dispatch Queue 的,这很重要。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t q2 = dispatch_queue_create("yogy.jianshu.gcd", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t gp = dispatch_group_create();
    
    dispatch_group_async(gp, q1, ^{ NSLog(@"www.jianshu.yogy -> q1 . 1"); });
    dispatch_group_async(gp, q1, ^{ NSLog(@"www.jianshu.yogy -> q1 . 2"); });
    dispatch_group_async(gp, q2, ^{ NSLog(@"www.jianshu.yogy -> q2 . 1"); });
    dispatch_group_async(gp, q2, ^{ NSLog(@"www.jianshu.yogy -> q2 . 2"); });
    
    dispatch_group_notify(gp, dispatch_get_main_queue(), ^{ NSLog(@"www.jianshu.yogy -> done"); });
    // 输出为:
    // www.jianshu.yogy -> q2 . 2   
    // www.jianshu.yogy -> q2 . 1
    // www.jianshu.yogy -> q1 . 1
    // www.jianshu.yogy -> q1 . 2
    // www.jianshu.yogy -> done
    // 前四个输出的顺序不一定,但 done 总是会最后输出。
}   

使用 dispatch_group_notify 方法,一旦检测到所有加入到 Group 中的处理执行结束后,就将结束处理追加到指定的 Dispatch Queue 中。


另外,在 Dispatch Group 中也可以使用 dispatch_group_wait 函数仅等待全部处理执行结束。

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 会一直阻塞当前线程直到 group 中的所有处理全部执行完成。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
    // 返回 0,表示 group 中的全部处理都执行结束
} else {
    // 否则,表示 group 中的处理为执行结束
}

dispatch_group_wait 会阻塞当前线程是指,一旦调用了 dispatch_group_wait 函数,该函数就处于调用状态而不返回。即执行 dispatch_group_wait 函数的线程停止,直到经过了 dispatch_group_wait 函数中指定的时间或属于指定 Dispatch Group 的处理全部执行结束。

dispatch_barrier_async

该函数必须同 dispatch_queue_create 函数生成的 Concurrent Dispatch Queue 一起使用。同 dispatch_get_global_queue 得到的 Concurrent Dispatch Queue 一起使用不会生效。

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);

dispatch_barrier_async 函数会等待当前的 queue 中已有的处理全部执行完后再添加 blk_for_writing,并且还保证不会添加其他的处理,只有当 blk_for_writing 执行完后才会恢复为一般的 Concurrent Dispatch Queue,后面的处理又可以并行执行了。

dispatch_sync

dispatch_sync 函数会阻塞当前进程直到指定的 Block 被执行完成。它使当前线程进入该函数,但是要等到指定的 Block 被执行完才返回。该方法容易引发死锁。

dispatch_queue_t queue = dispatch_queue_create("yogy.jianshu.gcd", NULL);
dispatch_async(queue, ^{
    NSLog(@"enter...");
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"www.jianshu.yogy");
    });
    NSLog(@"exit...");
});
// 会发生死锁,输出只有
// enter...
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    NSLog(@"enter...");
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"www.jianshu.yogy");
    });
    NSLog(@"exit...");
});
// 不会出现死锁,输出为:
// enter...
// www.jianshu.yogy
// exit...

从上面两个例子看出:dispatch_sync 容易对 Serial Dispatch Queue 引起死锁。解析一下引起死锁的原因。对于 queue 为 Serial Dispatch Queue 的时候,使用 dispatch_sync(queue, ^{}) 已经把 Block 加入到 queue 中,但是由于 queue 是串行执行的队列且当前正在执行中,所以加入的 Block 没有机会执行,而当前线程又再等待 Block 执行结束。这样就发生死锁了。而对于 queue 为 Concurrent Dispatch Queue 的时候,使用 dispatch_sync(queue, ^{}) 同样把 Block 加入到了 queue 中,但不同的是 queue 可并行执行,也就是 Block 可以被加入到其他的线程中执行,于是等 Block 在其他线程执行完后,当前线程就可以继续执行了。但是当 queue 为 Concurrent Dispatch Queue 时还是存在潜在问题的,比如当前的所有线程都处于等待状态的话,同样就没有 Block 能执行了。如下面的代码:

static long cnt = 0;
dispatch_queue_t queue = dispatch_queue_create("yogy.jianshu.gcd", DISPATCH_QUEUE_CONCURRENT);
// 如果使用系统的 Global Dispatch Queue 就不会死锁,应该是系统内部实现不同。
// 如果不注释下面一行,不会出现死锁。
// queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000000; ++i) {
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:5.0];
        dispatch_sync(queue, ^{
            NSLog(@"%ld", ++cnt);
        });
    });
}
NSLog(@"www.jianshu.yogy");

dispatch_apply

dispatch_apply 也会阻塞当前线程,直到指定的 Block 执行完指定的次数。书中说:dispatch_applydispatch_sync 和 Dispatch Group 的关联 API,不知道怎么解释哈。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zu", index);
});
NSLog(@"www.jianshu.yogy");
// 下标的输出顺序不定,但是 www.jianshu.yogy 一定会最后输出。

由于 dispatch_apply 函数也与 dispatch_sync 函数相同,会等待处理执行结束,因此推荐在 dispatch_async 函数中非同步执行 dispatch_apply 函数。

dispatch_suspend / dispatch_resume

参数为一个队列。这些函数对已经在执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

dispatch_semaphore

这玩意能在多线程之间进行通信,一般我们认为某一个处理不能和另一个处理同时进行,但我们都是相对于整个处理而言的。可是如果某一个处理的某一段代码不能与另一个处理的某一段代码并行执行,而其他地方又可以并行执行呢?这时候,dispatch_semaphore 就派上用场呢。

// 创建一个计数值为 1 的 dispatch_semaphore_t
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// 等待 semaphore 的计数值 >= 1,并将计数值减 1 后返回,会阻塞当前线程。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);
// 等待 time 时间或 semaphore 的计数值 >= 1 返回。
// 返回值为 0,则等待到了有效信号
// 否则,只是达到了 time 时间,但并没有得到有效信号
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
    
for (int i = 0; i < 10; ++i) {
    dispatch_async(queue, ^{
        // 一直等到有效信号,并将计数值减 1
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        [array addObject:[NSNumber numberWithInt:i]];
        NSLog(@"%d", i); // 0-9 依次输出
        // 发出信号后,会按 dispatch_semaphore_wait 的等待顺序依次执行,FIFO。
        dispatch_semaphore_signal(semaphore);
    });
}

dispatch_once

常用于单例模式,对于同一个 dispatch_once_t,程序只会跑一次。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只被执行依次的代码
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容