iOS Grand Central Dispatch 的使用

什么是 GCD ?

GCD 全称为 Grand Central Dispatch , 是一个异步执行任务的技术之一。 一般将应用程序中线程管理用的代码再系统级中实现。 开发者只需要定义想执行的任务并追加到适当的 Dispatch Queue 中, GCD 就能生成必要的线程并计划执行任务。 由于线程管理是作为系统的一部分来实现的, 因此可统一管理,也可执行任务,这样就比以前的线程更有效率。

相对于直接去写多线程的代码。通过 GCD 去使用多线程技术不仅非常简单,并且避免一些多线程操作中可能出现的问题。开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。

Dispatch Queue

Dispatch Queue 介绍

Dispatch Queue 是执行等待任务的队列。在使用 GCD 执行任务时,添加任务到对应的 Dispatch Queue 中。由 Dispatch Queue 控制怎么在线程上执行任务。Dispatch_Queue 按照任务追加的顺序执行(FIFO)。 Dispatch Queue 分类两种类型:

  • Serial Dispatch Queue: 是一个串行队列,添加到这个队列中的任务会按照顺序一个任务执行完后才会执行下一个任务。
  • Concurrent Dispatch Queue: 是一个并行队列,添加到这个队列中的任务会多个任务同时执行。但是具体同时能执行多少个任务,这个XNU 内核 去帮我们管理的。

创建 Dispatch Queue

获取上面两种 Dispatch Queue 可以通过 dispatch_queue_create 函数去创建。这个函数返回代表 Dispatch Queuedispatch_queue_t 类型变量。

dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
// 其中第一个参数传入一个 C 字符串,作为名字来标识一个 Dispatch Queue ,这个名字在调试中非常有用,会显示在调试面板上。
// 第二个参数如果创建 Serial Dispatch Queue 则填入 NULL 。 
// 如果需要创建 Concurrent Dispatch Queue 则填入DISPATCH_QUEUE_CONCURRENT。

获取系统提供的 Dispatch Queue

如果不自己创建 Dispatch Queue ,系统也开发者提供了Main Dispatch Queue Global Dispatch Queue

dispatch_queue_t mainQueue             = dispatch_get_main_queue();
dispatch_queue_t globalQueueHigh       = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t globalQueueDefault    = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t globalQueueLow        = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t globalQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

// dispatch_get_main_queue 方法获得的是 Serial Dispatch Queue,主线程就是在这个 Dispatch Queue 内。
// dispatch_get_global_queue 方法获得都是 Concurrent Dispatch Queue。其第一个参数表示优先级,第二是保留字段(填0);

dispatch_async & dispatch_sync

dispatch_async & dispatch_sync 介绍

dispatch_asyncdispatch_sync 这两个函数都可以把 Block任务 绑定到指定的 Dispatch Queue 中执行。区别从方法名中就可以看出。dispatch_async 是异步执行,dispatch_sync 是同步执行。看具体例子:

NSLog(@"1...");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    sleep(1); NSLog(@"dispatch_sync");
});
NSLog(@"2...");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    sleep(1); NSLog(@"dispatch_async");
});
NSLog(@"3...");
// 上面这段代码执行的输出是:
2017-02-22 21:47:18.545 OCGCD[47679:28023198] 1...
2017-02-22 21:47:19.605 OCGCD[47679:28023198] dispatch_sync
2017-02-22 21:47:19.606 OCGCD[47679:28023198] 2...
2017-02-22 21:47:19.606 OCGCD[47679:28023198] 3...
2017-02-22 21:47:20.675 OCGCD[47679:28023256] dispatch_async

可以看出 dispatch_sync 这个函数会等到其 Block 内的任务执行完毕才会返回,然后继续往下执行。dispatch_async 则是直接返回继续往下执行,其 Block 内的任务会异步的去执行。

dispatch_async & dispatch_sync 和线程

上面说过 GCD 的背后是系统内核在管理线程,那 dispatch_asyncdispatch_sync 执行相关了队列和线程那些关联呢? 看具体例子:

dispatch_queue_t serial     = dispatch_queue_create("io.tao.serial", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrent = dispatch_queue_create("io.tao.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t mainQueue  = dispatch_get_main_queue();

dispatch_sync (serial,     ^{ NSLog(@"1: %@", [NSThread currentThread]); });
dispatch_async(serial,     ^{ NSLog(@"2: %@", [NSThread currentThread]); });
dispatch_sync (concurrent, ^{ NSLog(@"3: %@", [NSThread currentThread]); });
dispatch_async(concurrent, ^{ NSLog(@"4: %@", [NSThread currentThread]); });
dispatch_async(mainQueue,  ^{ NSLog(@"5: %@", [NSThread currentThread]); });
// 上面这段代码执行的输出是:
2017-02-22 22:29:04.783 OCGCD[50871:28074784] 1: <NSThread: 0x6080000646c0>{number = 1, name = main}
2017-02-22 22:29:04.783 OCGCD[50871:28074818] 2: <NSThread: 0x60000006ba40>{number = 3, name = (null)}
2017-02-22 22:29:04.783 OCGCD[50871:28074784] 3: <NSThread: 0x6080000646c0>{number = 1, name = main}
2017-02-22 22:29:04.784 OCGCD[50871:28074819] 4: <NSThread: 0x60000006b800>{number = 4, name = (null)}
2017-02-22 22:29:04.789 OCGCD[50871:28074784] 5: <NSThread: 0x6080000646c0>{number = 1, name = main}

由上面的例子得出,无论是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue

通过 dispatch_sync 函数执行的都会在当前线程中执行任务。

通过 dispatch_async 函数执行的都会在子线程中执行任务。

但是有一个例外是 使用 dispatch_async 执行 mainQueue 队列也是在当前线程中执行的。永远都只会在主线程中执行。

dispatch_sync 和 死锁

细心的人可能已经发现,上面的例子没有出现下面这行代码:

dispatch_sync(mainQueue,  ^{ NSLog(@"6: %@", [NSThread currentThread]); });

因为这行代码在主线程中执行时会产生死锁,程序会 Carsh 。产生死锁的原因是: 因为该代码在 Main Dispatch Queue(主线程) 中使用 dispatch_syncMain Dispatch Queue(主线程) 上执行 Block 任务Main Dispatch Queue(主线程) 在一直等着 dispatch_sync 函数返回再继续执行,但是 dispatch_sync 又需要在 Main Dispatch Queue(主线程) 执行Block 任务,所以这就像是一个死胡同,永远也等不到 dispatch_sync 函数返回。下面这个例子也是同样的问题:

dispatch_queue_t serial = dispatch_queue_create("io.tao.q1", DISPATCH_QUEUE_SERIAL);
dispatch_async(serial, ^{
    dispatch_sync(serial, ^{ NSLog(@"Are you ok?"); });
});

所以在使用 dispatch_sync 函数去执行 Serial Dispatch Queue 队列时,要特别小心死锁的问题。在当前 Dispatch Queue 上去让 dispatch_queue 在当前 Dispatch Queue 执行任务时就会产生死锁。

dispatch_after

dispatch_after 介绍

dispatch_after 可以让 Block 任务 在指定时间后加入到 Dispatch Queue 中执行,并非在指定时间后执行 Block 任务

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

// 上面的例子等同于 3 秒之后调用下面的代码
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_after");
});

dispatch_time 介绍

上面的例子中 dispatch_after 第一个参数是指定时间 dispatch_time_t 类型的值。dispatch_time_t 函数定义如下:

dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
// 第一个参数指定时间的开始, 一般使用 DISPATCH_NOTHROW 表示现在的时间。
// 第二个参数表示延后的时间,默认单位是纳秒, 可以 * NSEC_PER_SEC NSEC_PER_MSEC NSEC_PER_USEC 相关时间单位倍数来转换

dispatch_apply

dispatch_apply 介绍

dispatch_apply 可以多次执行一个队列里的 Block 任务,它会为 Block 任务提供一个计次的参数。 它和 dispatch_sync 一样,需要等到所有 Block 任务执行完,这个函数才会返回。 所以也要主要死锁的问题,当队列是 Serial Dispatch Queue 时,不要在当前 Dispatch Queue 上去让 dispatch_apply 在当前 Dispatch Queue 执行任务。

dispatch_apply(3, dispatch_get_global_queue(0, 0), ^(size_t index) {
    NSLog(@"%zu", index);
});
NSLog(@"done");
// 上面的例子的输出,done 总是在最后输出。刚好可以验证上面的说法。dispatch_applay 是同步去执行的
2017-02-23 13:23:27.041 OCGCD[14310:28921393] 1
2017-02-23 13:23:27.042 OCGCD[14310:28921396] 2
2017-02-23 13:23:27.042 OCGCD[14310:28921344] 0
2017-02-23 13:23:27.043 OCGCD[14310:28921344] done

dispatch_set_target_queue

用其设置队列的优先级

上面有例子通过 dispatch_get_global_queue 去生成不同优先级的队列。通过 dispatch_queue_create 函数生成的 Dispatch Queue 的优先级都是默认的,如果要改变它们优先级可以通过 dispatch_set_target_queue ,看具体例子:

dispatch_queue_t queueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue     = dispatch_queue_create("io.tao.serial", NULL);
dispatch_set_target_queue(queue, queueHigh);
// 上面的例子,把默认优先级的 queue, 设置成和 queueHigh 一样的高优先级
// 第一个参数填入要设置的 Dispatch Queue
// 第二个参数填入要参照的目标 Dispatch Queue

设置多个 Serial Dispatch Queue 的执行顺序

多个 Serial Dispatch Queue 是并行执行的。但是开发碰到了要让它们串行执行的需求时,也可以通过 dispatch_set_target_queue 方法来实现。只需要把多个 Serial Dispatch Queue 设置成同一个目标 Serial Dispatch Queue 就可以了。看具体例子:

dispatch_queue_t serialQueue  = dispatch_queue_create("io.tao.serial", NULL);
dispatch_queue_t serialQueue1 = dispatch_queue_create("io.tao.app.serial1", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("io.tao.app.serial2", NULL);
dispatch_queue_t serialQueue3 = dispatch_queue_create("io.tao.app.serial3", NULL);

dispatch_set_target_queue(serialQueue1, serialQueue);
dispatch_set_target_queue(serialQueue2, serialQueue);
dispatch_set_target_queue(serialQueue3, serialQueue);

NSLog(@"start...");
dispatch_async(serialQueue1, ^{
    sleep(1);
    NSLog(@"serialQueue1");
});
dispatch_async(serialQueue2, ^{
    NSLog(@"serialQueue2");
});
dispatch_async(serialQueue3, ^{
    NSLog(@"serialQueue3");
});
NSLog(@"end...");
// 下面是上面例子的控制台输出。
// serialQueue1, serialQueue2, serialQueue3 成功的按照顺序输出了。 
2017-02-23 14:15:21.033 OCGCD[18304:28985032] start...
2017-02-23 14:15:21.033 OCGCD[18304:28985032] end...
2017-02-23 14:15:22.105 OCGCD[18304:28985092] serialQueue1
2017-02-23 14:15:22.105 OCGCD[18304:28985092] serialQueue2
2017-02-23 14:15:22.105 OCGCD[18304:28985092] serialQueue3

dispatch_group

dispatch_group 介绍

dispatch_group 允许把多个 Dispatch Queue 添加到一个 Group 中,等 Group 中所有的 Dispatch Queue 执行完成后,Group 会执行添加的特定 Block 任务。无论是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue 都是有效的。 这在实际开发中非常有用。看具体例子:

dispatch_queue_t serial     = dispatch_queue_create("io.tao.serial", NULL);
dispatch_queue_t concurrent = dispatch_queue_create("io.tao.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group      = dispatch_group_create();

dispatch_group_async(group, serial,     ^{ sleep(3); NSLog(@"serial1");});
dispatch_group_async(group, serial,     ^{ sleep(2); NSLog(@"serial2");});
dispatch_group_async(group, concurrent, ^{ sleep(1); NSLog(@"concurrent1");});
dispatch_group_async(group, concurrent, ^{ sleep(2); NSLog(@"concurrent2");});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"Group 中所有的 Dispatch Queue 已执行完");
});
// 下面是上面例子的控制台输出。
2017-02-23 14:30:06.566 OCGCD[19452:29004031] concurrent1
2017-02-23 14:30:07.494 OCGCD[19452:29004033] concurrent2
2017-02-23 14:30:08.494 OCGCD[19452:29004030] serial1
2017-02-23 14:30:10.561 OCGCD[19452:29004030] serial2
2017-02-23 14:30:10.561 OCGCD[19452:29003990] Group 中所有的 Dispatch Queue 已执行完

在添加到 Dispatch Group 中的所有任务全部执行结束后, 就会把 dispatch_group_notify 中的 Block 任务 添加对指定队列中执行。来做一些结束工作。

dispatch_group_wait 的使用

除了通过 dispatch_group_notify 获得 Dispatch Group 执行完的通知,也能通过 dispatch_group_wait 函数来获取结束通知。这个函数会一直等着 Dispatch Group 执行完成才会返回。 它就像是一个断点,把当前线程给断住,直到 Group 的所有任务执行完成。

dispatch_queue_t concurrent = dispatch_queue_create("io.tao.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group      = dispatch_group_create();

dispatch_group_async(group, concurrent, ^{ sleep(3); NSLog(@"concurrent1");});
dispatch_group_async(group, concurrent, ^{ sleep(2); NSLog(@"concurrent2");});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"Dispatch Group 中所有任务都已执行完毕");
// 上面例子的控制台输出。刚好能证明上面的观点
2017-02-23 14:44:16.883 OCGCD[20511:29021074] concurrent2
2017-02-23 14:44:17.881 OCGCD[20511:29021077] concurrent1
2017-02-23 14:44:17.882 OCGCD[20511:29021036] Dispatch Group 中所有任务都已执行完毕

dispatch_barrier_async

dispatch_barrier_async 介绍

Barrier 如同它的名字一样,它就像一个屏障把 Concurrent Dispatch Queue 里的多个任务给隔离开了。 Concurrent Dispatch Queue里的 Block 任务 是并行执行的,有时候想控制它们执行一部分 Block 任务 后,再执行特定操作,最后执行剩下的其他的 Block 任务dispatch_barrier_async 就可以帮我们做到。看具体例子:

dispatch_queue_t concurrent = dispatch_queue_create("io.tao.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrent, ^{NSLog(@"concurrent 1");});
dispatch_async(concurrent, ^{NSLog(@"concurrent 2");});
dispatch_async(concurrent, ^{NSLog(@"concurrent 3");});
dispatch_barrier_sync(concurrent, ^{ sleep(3); NSLog(@"barrier");});
dispatch_async(concurrent, ^{NSLog(@"concurrent 4");});
dispatch_async(concurrent, ^{NSLog(@"concurrent 5");});
dispatch_async(concurrent, ^{NSLog(@"concurrent 6");});
// 上面例子的控制台输出。注意每条 Log 的时间,concurrent 3 到 barrier 间隔了3秒
2017-02-23 15:17:04.968 OCGCD[22872:29058218] concurrent 1
2017-02-23 15:17:04.968 OCGCD[22872:29058220] concurrent 2
2017-02-23 15:17:04.969 OCGCD[22872:29058217] concurrent 3
2017-02-23 15:17:08.039 OCGCD[22872:29058165] barrier
2017-02-23 15:17:08.040 OCGCD[22872:29058217] concurrent 4
2017-02-23 15:17:08.040 OCGCD[22872:29058220] concurrent 5
2017-02-23 15:17:08.040 OCGCD[22872:29058218] concurrent 6

Dispatch Semaphore

Dispatch Semaphore 是基于计数的信号,在 GCD 中控制并发队列同步的方法之一。 多个队列同时访问修改一个数据时,可能会产生数据不一致的情况,又是程序还会 Carsh。这是就可以通过 Dispatch Semaphore 去解决。

  • dispatch_semaphore_create(1); 会创建一个指定数字的信号量,这里是1。
  • dispatch_semaphore_wait 函数当信号量 >= 1时会把信号量-1,并返回函数 。否则则会一直卡在此处,或者等到超时。
  • dispatch_semaphore_signal 发送一个信号,让信号量+1。
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *marr = [NSMutableArray array];
for (int i=0; i < 9999; ++i) {
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        [marr addObject:[NSNumber numberWithInt:i]];
        dispatch_semaphore_signal(semaphore);
    });
}
// 上面例子执行完后,marr 里会包含 9999个 NSNumber 对象。因为通过 Dispatch Semaphore 控制始终只有一个线程在操作 marr。所以没有数据竞争的问题

dispatch_suspend / dispatch_resume

这两个函数可以控制 Dispatch Queue 暂停(suspend) 和 恢复(resume)。当 dispatch_suspend(queue) 执行时 queue 被暂停。已经加入到 queue 中的任务会继续执行。后面在再加入的任务会暂停,一直等到 dispatch_resume(queue) 被执行。看具体例子:

dispatch_queue_t queue = dispatch_queue_create("io.tao.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{ sleep(2); NSLog(@"queue1"); });
dispatch_async(queue, ^{ NSLog(@"queue2"); });

NSLog(@"queue supend...");
dispatch_suspend(queue);
dispatch_async(queue, ^{ NSLog(@"queue3"); });

sleep(8);
NSLog(@"queue resume...");
dispatch_resume(queue);
// 上面例子的控制台输出。注意每条 Log 的时间。刚好能证明上面的结论
2017-02-23 16:30:19.829 OCGCD[28759:29155110] queue supend...
2017-02-23 16:30:19.829 OCGCD[28759:29155170] queue2
2017-02-23 16:30:21.889 OCGCD[28759:29155168] queue1
2017-02-23 16:30:27.903 OCGCD[28759:29155110] queue resume...
2017-02-23 16:30:27.904 OCGCD[28759:29155167] queue3

GCD 的内存管理

在 iOS6 or Mac OS X 10.8 以前 GCD 对象并没有纳入 ARC 管理范围。通过带 dispatch_xxx_create 创建的值都需开发者负责释放。dispatch_retain dispatch_release 。iOS6 or Mac OS X 10.8 之后使用的是 ARC 的话就不需要手动管理内存了。

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

推荐阅读更多精彩内容