Grand Central Dispatch (GCD) 介绍

GCD 概要

什么是 GCD

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


dispatch_async (queue, ^{
    /**
    *   长时间处理
    *
    *   例如 AR 用画像识别
    *   例如数据库访问
    */
    .....
    
    /*
    *   长时间处理结束,主线程使用改处理结果
    */
    dispatch_async (dispatch_get_main_queue(), ^{
        /*
        *   只在主线程可以执行的处理
        *
        *    例如用户刷新页面
        */
    });
});

多线程编程

  • 线程: 线程是指程序的一个指令执行序列
  • 多线程:
  • 上下文切换:
  • 多线程编程:

多线程编程实际上是一种易发生各种问题的编程技术。

  • 数据竞争
  • 死锁
  • 太多线程导致消耗大量内存

尽管极易发生各种问题,也应当使用多线程编程。因为使用多线程编程可保证应用程序的响应性能。
GCD 大大简化了偏于复杂的多线程编程的源代码。

GCD 的 API

Dispatch Queue

首先回顾下苹果官方对 GCD 的说明。

开发者要做的只是定义向执行的任务并追加到适当的 Dispatch Queue 中。

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

Dispatch Queue是执行处理的等待队列。应用程序编程人员通过 dispatch_async 函数等 API ,在 Block 语法中技术想执行的处理并将其追加到 Dispatch Queue。 Dispatch Queue 按照追加的顺序 (先进先出 FIFO)执行处理。

Dispatch Queue 的种类 说明
Serial Dispatch Queue 使用一个线程,等待现在执行中处理结束
Concurrent Dispatch Queue 使用多线程,不等待现在执行中处理结束

如何得到这些 Dispatch Queue 呢?有两种方法。

dispatch_queue_create

第一种方法

// Serial Dispatch Queue
dispatch_queue_t mySerialDispatchQueue = 
    dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
    
//  Concurrent Dispatch Queue
dispatch_queue_t myConcurrentDispatchQueue = 
    dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(myConcurrentDispatchQueue, ^{
    NSLog(@"block on myConcurrentDispatchQueue");
});
    
//dispatch_release(myConcurrentDispatchQueue); //ARC 中不需要

Serial Dispatch Queue 生成个数的注意事项

  • 只在为了避免多线程编程问题之一---数据竞争时使用 Serial Dispatch Queue
  • 使用 dispatch_queue_create 可以生成任意多个 Dispatch Queue, 如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。

dispatch_queue_create 命名规则:推荐使用应用程序 ID 这种逆序全程域名。该名称现在 Xcode 的 Instruments 的调试器中作为 Dispatch Queue 的名称。另外这个名称海显示在 Crash Log 中。

Main Dispatch Queue / Global Dispatch Queue

第二种方法是获取系统标准提供的 Dispatch Queue

 /*
     *  Main Dispatch Queue 的获取方法
     */
    dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
    
    /*
     *  Global Dispatch Queue (高优先级) 的获取方法
     */
    dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

    /*
     *  Global Dispatch Queue (默认优先级) 的获取方法
     */
    dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    /*
     *  Global Dispatch Queue (低优先级) 的获取方法
     */
    dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    
    /*
     *  Global Dispatch Queue (后台优先级) 的获取方法
     */
    dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

但是通过 XNU 内核用于 Global Dispatch Queue 的线程,不能保证实时性,因此执行优先级只是大致判断。
另外对于 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 和 dispatch_release 并不会引起任何变化,也不会有任何问题。

/*
 *  在默认优先级的 Global Dispatch Queue 中执行 block
 */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    /*
     *  可并行执行的处理
     */
    
    /*
     *  在 Main Dispatch Queue 中执行 block
     */
    dispatch_async(dispatch_get_main_queue(), ^{
        
        /*
         *  只能在主线程中执行的处理
         */
    });
});

dispatch_set_target_queue

dispatch_queue_create 函数生成的 Dispatch Queue 都使用与默认优先级的 Global Dispatch Queue 相同执行优先级的线程。而变更生成的 Dispatch Queue 的执行优先级要使用 dispatch_queue_create 函数。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

将 Dispatch Queue 指定为 dispatch_set_target_queue 函数的参数, 不仅可以变更 Dispatch Queue 的执行优先级,还可以作为 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(@"waited at least three seconds.");
});

dispatch_time 计算相对时间

dispatch_walltime 计算绝对时间

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 Queue 中的多个处理全部结束想执行结束处理,这种情况会经常出现。在此种情况下使用 Dispatch Group,这些 Block 如果全部执行完毕,就会执行 Main Dispatch Queue 中结束处理用的 Block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk0");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk2");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"done");
    });
    
    // 阻塞线程等待结束,
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_wait 函数的返回值不为0,就意味着虽然经过了指定的时间,但是属于 Dispatch Group 的某一个处理还在执行中。如果返回值为0,那么全部处理执行结束。

dispatch_barrier_sync

在访问数据或文件时,使用 Sirial Dispatch Queue 可避免数据竞争的问题。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_sync(queue, blk_for_writing);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);

dispatch_sync

dispatch_async 函数的 『async』意味着『非同步』(asynchronous),就是将指定的 Block 『非同步』地追加到指定的 Dispatch Queue 中。dispatch_sync 函数会一直等待。

dispatch_apply

把一项任务提交到队列中多次执行,具体是并行执行还是串行执行由队列本身决定.注意,dispatch_apply不会立刻返回,在执行完毕后才会返回,是同步的调用。

同 dispatch_sync 一样 也是同步的函数,会一直等待,直到所有的任务都完成。

dispatch_suspend / dispatch_resume

  • dispatch_suspend : 挂起
  • dispatch_resume : 恢复

Dispatch Semaphore

  1. 创建信号量,可以设置信号量的资源数。0表示没有资源,调用dispatch_semaphore_wait会立即等待。dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

  2. 等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

  3. 通知信号,如果等待线程被唤醒则返回非0,否则返回0。

    dispatch_semaphore_signal(semaphore);

最后,还是回到生成消费者的例子,使用dispatch信号量是如何实现同步:

dispatch_semaphore_t sem = dispatch_semaphore_create(0);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //消费者队列

      while (condition) {

    if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC))) //等待10秒

      continue;

    //得到数据

  }

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //生产者队列

      while (condition) {

     if (!dispatch_semaphore_signal(sem))

    {

      sleep(1); //wait for a while

      continue;

    }

    //通知成功

  }

});

dispatch_once

 static int initialized = NO;
        
if (initialized == NO) {
    /*
     *  初始化
     */
    initialized = YES;
}

如果使用dispatch_once

static dispatch_once_t pred;
dispatch_once(&pred, ^{
    /*
     *  初始化
     */
});

使用 dispatch_once,在多线程模式中仍然安全。

常用于单例模式中生成单例对象。

GCD 术语

Serial vs. Concurrent 串行 vs. 并发

这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。

Synchronous vs. Asynchronous 同步 vs. 异步

在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个同步函数只在完成了它预定的任务后才返回。

一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。

注意——当你读到同步函数“阻塞(Block)”当前线程,或函数是一个“阻塞”函数或阻塞操作时,不要被搞糊涂了!动词“阻塞”描述了函数如何影响它所在的线程而与名词“代码块(Block)”没有关系。代码块描述了用 Objective-C 编写的一个匿名函数,它能定义一个任务并被提交到 GCD 。

译者注:中文不会有这个问题,“阻塞”和“代码块”是两个词。

Critical Section 临界区

就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(译者注:它的值不再可信)。

Race Condition 竞态条件

这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。

Deadlock 死锁

两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。

Thread Safe 线程安全

线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。

Context Switch 上下文切换

一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

Queues 队列

GCD 提供有 dispatch queues 来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。

所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。

在本节你会看到两种调度队列,都是由 GCD 提供的,然后看一些描述如何用调度函数添加工作到队列的例子。

Serial Queues 串行队列

串行队列中的任务一次执行一个,每个任务只在前一个任务完成时才开始。而且,你不知道在一个 Block 结束和下一个开始之间的时间长度,如下图所示:

Concurrent Queues 并发队列

在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD 。

Queue Types 队列类型

首先,系统提供给你一个叫做 主队列(main queue) 的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView 或发送通知的。

系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues) 。目前的四个全局队列有着不同的优先级:backgroundlowdefault 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。

最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。

以上是调度队列的大框架!

GCD 的“艺术”归结为选择合适的队列来调度函数以提交你的工作。体验这一点的最好方式是走一遍下边的列子,我们沿途会提供一些一般性的建议。

GCD 实现

Dispatch Queue

  • 用于管理追加的 Block 的 C 语言层实现的 FIFO 队列
  • Atomic 函数中实现的用于排他控制的轻量级信号
  • 用于管理线程的 C 语言层实现的一些容器

通常,应用程序中编写的线程管理用的代码要在系统级实现

GCD 在系统级即 IOS 和 OSX 的核心 XNU 内核上实现。因此,无论编程人员如何努力编写管理线程的代码,在性能方面也不可能胜过 XNU 内核级所实现的 GCD。

组件名称 提供技术
libdispatch Dispatch Queue
Libc (pthreads) pthread_workqueue
XNU 内核 workqueue

参考

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

推荐阅读更多精彩内容