读书笔记-《Objective-C高级编程》之Grand Central Dispatch

Grand Central Dispatch

3.1 Grand Central Dispatch(GCD)摘要

3.1.1 什么是GCD

Grand Central Dispatch(GCD)是异步执行的任务之一。

3.1.2 多线程编程

在Objective-C的if语句和for语句等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置偏移)。但是,由于一个CPU一次只能执行一个命令列,不执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其整不会出现分歧。

通过CPU执行的CPU命令列

这里所说的“一个CPU执行的CPU命令列为一条无分叉路径”即为线程。这种无分叉路径不止一条,存在多条即为“多线程。”在多线程中,一个CPU核执行多条不同路径上的不同命令。

在多线程中执行CPU命令列

基本上一个CPU核一次能够执行的CPU命令始终为1.

多线程的问题:数据竞争、死锁、太多线程导致消耗大量内存等。

多线程可以保证应用程序的响应性能。应用程序启动时,通过最先执行的线程,即“主线程”来描绘用户界面、处理触摸屏幕的事件等。如果该主线程中进行长时间的处理,如AR用画像的识别或数据库访问,就会妨碍主线程德执行(阻塞)。在OSX和iOS的应用程序中,会妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面,应用程序的书面长时间停滞等问题。

多线程的优点

使用多线程,在执行长时间的处理时仍可保证用户界面的响应性能。

3.2 GCD的API

3.2.1 Dispatch Queue

GCD官方说明:开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。

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

将Block通过dispatch_async函数"追加"赋值在变量queue的“Dispatch Queue中”。Dispatch Queue按照追加的顺序(先进先出FIFO)执行处理

通过Dispatch Queue执行处理

在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Disaptch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。

Dispatch Queue 的种类 说明
Serial Dispatch Queue 等待现在执行中处理结束
Concurent Dispatch Queue 不等待现在执行中处理结束
Serial Dispatch Queue 和 Concurent Dispatch Queue

并行执行的处理数量取决于当前系统的状态。即 iOS 和 OSX 基于 Dispatch Queue 中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定 Concurent Dispatch Queue 中并行执行的处理数。所谓“并行执行”,就是使用多个线程执行多个处理。

Serial Dispatch Queue、 Concurent Dispatch Queue 和线程的关系

iOS 和 OSX 的核心————XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的内核。XNU内核仅使用 Concurent Dispatch Queue 便可完美地管理并行执行多个处理的线程。

3.2.2 dispatch_queue_create

通过dispatch_queue_create函数可生成 Dispatch Queue。以下源代码生成了 Serial Dispatch Queue。

    dispatch_queue_t serialQueue = 
      dispatch_queue_create("com.example.gcddemo.serialqueue", NULL);

在说明dispatch_queue_create函数之前,先讲一下关于 Serial Dispatch Queue 生成个数的注意事项。

如前所述, Concurrent Dispatch Queue 并行执行多个追加处理,而 Serial Dispatch Queue 同时只能追加一个处理。虽然 Serial Dispatch Queue 和 Concurrent Dispatch Queue 受到系统资源的限制, 但用dispatch_queue_create函数可生成任意多个 Dispatch Queue。

当生成多个 Serial Dispatch Queue 时,各个 Serial Dispatch Queue将并行执行。虽然在一个 Serial Dispatch Queue 中同时只能执行一个追加处理,但如果将处理分别追加到4个 Serial Dispatch Queue 中, 各个 Serial Dispatch Queue 执行一个,即为同时执行四个处理(串行异步)。

多个 Serial Dispatch Queue

如果过多使用多线程,就会消耗大量内存,引发大量的上下文切换,大幅度降低系统的响应性能。

多个 Serial Dispatch Queue 引发的问题

只在为了避免多线程编程问题之一————多个线程更新想用资源所导致数据竞争时使用 Serial Dispatch Queue。

Serial Dispatch Queue 的用途

继续说明dispatch_queue_create函数,第一个参数为生成的 queue 的名称,推荐使用应用程序ID这种逆序全程域名(FQDN, fully qualified domain name),便于在调试和日志中定位问题。第二个参数,生成 Serial Dispatch Queue 时可为 DISPATCH_QUEUE_SERIAL 或 NULL, 生成 Concurent Dispatch Queue 时为DISPATCH_QUEUE_CONCURRENT

iOS6.0后,Dispatch Queue不再需要程序员负责释放(dispatch/_release)。

3.2.3 Main Dispatch Queue / Global Dispatch Queue

Main Dispatch Queue 是在主线程中执行的 Dispatch Queue。因为主线程只有一个,所以 Main Dispatch Queue 是 Serial Dispatch Queue。

追加到 Main Dispatch Queue 的处理在主线程的 RunLoop 中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到 Main Dispatch Queue 使用。这正好与NSObject类的 performSelectorOnMainThread实例方法这一执行的方法相同。

Main Dispatch Queue

Global Dispatch Queue 是所有应用程序都能够使用的 Concurrent Dispatch Queue。需要时获取使用即可

Global Dispatch Queue 有4个优先级,但是通过 XNU 内核用于 Global Dispatch Queue 的线程并不能保证实时性,因此执行优先级只是大致的判断。

Dispatch Queue的种类

名称 Dispatch Queue 的种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue (High Priority) Concurrent Dispatch Queue 执行优先级:高(最高优先)
Global Dispatch Queue (Default Priority) Concurrent Dispatch Queue 执行优先级:默认
Global Dispatch Queue (Low Priority) Concurrent Dispatch Queue 执行优先级:低
Global Dispatch Queue (Background Priority) Concurrent Dispatch Queue 执行优先级:后台

3.2.4 dispatch_set_target_queue

dispatch_set_target_queue是用来变更生成的 Dispatch Queue 的执行优先级的。

    dispatch_set_target_queue(<#dispatch_object_t  _Nonnull object#>, <#dispatch_queue_t  _Nullable queue#>)

指定要变更执行优先级的 Dispatch Queue 为dispatch_set_target_queue函数的第一个参数,指定与要使用的执行优先级相同的 Dispatch Queue 为第二个参数(目标)。

将 Dispatch Queue 指定为 dispatch_set_target_queue 函数的参数,不仅可以变更 Dispatch Queue 的执行优先级,还可以作成 Dispatch Queue 的执行阶层。如果多个 Serial Dispatch Queue 中使用dispatch_set_target_queue函数指定目标为某一个 Serial Dispatch Queue ,那么原本应并行执行的多个 Serial Dispatch Queue ,在目标 Serial Dispatch Queue 上只能同时执行一个处理。

Dispatch Queue 的执行层次

在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用dispatch_set_target_queue函数将目标指定为某一个 Serial Dispatch Queue,即可防止处理并行执行。

3.2.5 dispatch_after

dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到 Dispatch Queue。

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"waited after at least three seconds");
    });

因为 Main Dispatch Queue 在主线程的 RunLoop 执行,所以在比如每隔1/60秒执行的 RunLoop 中,Block 最快在3秒后,最慢在3秒+1/60秒后执行,并且在 Main Dispatch Queue 有大量处理追加或主线程处理本身有延迟时,这个时间会更长。

第一个参数是指定时间用的dispatch_time_t类型的值。该值使用dispatch_time函数(计算相对时间)或dispatch_walltime函数(计算绝对时间)。

3.2.6 Dispatch Group

当追加到的 Dispatch Queue 中的多个处理全部结束后想执行结束处理,使用 Dispatch Group。

    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");
    });

执行结果

    blk0
    blk1
    blk2
    done

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

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    if (result == 0) {
        // 属于 Dispatch Group 的全部处理执行结束
    } else {
        // 属于 Dispatch Group 的某一个处理还在执行
    }

dispatch_group_wait函数返回的等待时间为DISPATCH_TIME_FOREVER时,意味着永久等待,属于 Dispatch Group 的处理必定全部执行结束,因此返回值恒为0。

需要注意的是,一旦调用dispatch_group_wait函数,该函数就处于调用的状态而不返回。即执行dispatch_group_wait函数的现在的线程(当前线程)停止。在经过dispatch_group_wait函数中指定的时间或属于指定 Dispatch Group 的处理全部执行结束之前,执行该函数的线程停止。

指定DISPATCH_TIME_NOW,则不用任何等待即可判定属于 Dispatch Group 的处理是否执行结束。

3.2.7 dispatch_barrier_async

    dispatch_queue_t queue = dispatch_queue_create("com.example.gcddemo.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(dataQueue, ^{
        NSLog(@"read data 1");
    });
    dispatch_async(dataQueue, ^{
        NSLog(@"read data 2");
    });
    dispatch_barrier_async(dataQueue, ^{
        NSLog(@"write data 1");
    });
    dispatch_async(dataQueue, ^{
        NSLog(@"read data 3");
    });

dispatch_barrier_async函数会等待之前追加到 Dispatch Queue 中的处理全部执行结束后,再将dispatch_barrier_async函数指定的处理追加到 Dispatch Queue 中,然后在dispatch_barrier_async函数追加的处理执行完毕后,继续执行dispatch_barrier_async函数之后追加到 Dispatch Queue 的处理。

dispatch_barrier_async函数的处理流程

使用 Concurrent Dispatch Queue 和 dispatch_barrier_async函数可实现搞笑的数据库访问和文件访问。

3.2.8 dispatch_sync

dispatch_async(异步),指定的 Block ”非同步“地追加到指定的 Dispatch Queue 中,dispatch_async函数不做任何等待。

Dispatch_async 函数的处理流程

dispatch_sync(同步),将指定的Block”同步“追加到指定的 Dispatch Queue 中,在追加Block结束之前,dispatch_sync函数会一直等待。

Dispatch_sync 函数的处理流程

使用dispatch_sync函数需要注意死锁问题。

    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"123");
    });

该源码在主线程中执行指定的 Block,并等待其执行结束,而其实在主线程中正在执行这些源代码,所以无法追加到主线程的 Block。

3.2.9 dispatch_apply

da函数是ds函数和 dg 的关联API。该函数按指定的次数将指定的Block追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zu", index);
    });
    NSLog(@"done");

执行结果

    0
    1
    3
    2
    5
    4
    6
    7
    8
    9
    done

因为在 Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是输出结果最后的done必定在最后的位置上。这是因为da函数会等待全部处理执行结束。

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

对比for循环中使用dispatch_async函数,并行的机制比串行的机制更安全、快速。

3.2.10 dispatch_suspend/dispatch_resume

当追加大量处理到 Dispatch Queue 时,在追加处理的过程中,有时希望不执行已追加的处理。这种情况可以使用dispatch_suspend函数挂起指定的 Dispatch Queue,当可以执行时再使用dispatch_resume函数恢复指定的 Dispatch Queue。

dispatch_suspend函数不会立即停止正在执行的 Block,而是在当前 Block 执行完成后,暂停后续的 Block 执行。

3.2.11 Dispatch Semaphore

当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用 Serial Dispatch Queue 和dispatch_barrier_async函数可避免这类问题,但有必要进行更细粒度的排他控制,这时可以使用 Dispatch Semaphore。

Dispatch Semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗,可以通过时举起手旗,不可通过时放下手旗。而在 Dispatch Semaphore 中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时,减去1而不等待。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   
   /*
     生成 Dispatch Semaphore
     Dispatch Semaphore的计数初始值设定为“1”
     保证可访问NSMutableArray类对象的线程同时只有一个
    */
   dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
   NSMutableArray *array = [NSMutableArray array];
   
   for (int i = 0; i < 100000; i++) {
       dispatch_async(queue, ^{
           /*
             等待 Dispatch Semaphore
             一直等待,直到 Dispatch Semaphore 的计数值达到大于等于1
            */
           dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
           
           /*
             由于 Dispatch Semaphore 的计数值达到大于等于1
             所以将 Dispatch Semaphore 的计数值减去1
             dispatch_semaphore_wait函数执行返回。
             即执行到此时的 Dispatch Semaphore 的计数值恒为0
             由于可访问NSMutableArray类对象的线程只有1个
             因此可安全地进行更新
            */
           [array addObject:[NSNumber numberWithInt:i]];
           
           /*
             排他控制处理结束
             所以通过dispatch_semaphore_signal函数将
             Dispatch Semaphore 的计数值加1。
             如果有通过dispatch_semaphore_wait函数
             等待 Dispatch Semaphore 的计数值增加的线程
             就由最先等待的线程执行。
            */
           dispatch_semaphore_signal(semaphore);
       });
   }
   NSLog(@"done");

3.2.12 dispatch_once

dispatch_once函数式保证在应用程序执行中只执行一次指定处理的API。多线程环境下也可保证百分百安全。

    static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       // code to be executed once
   });

3.2.13 Dispatch I/O

在读取较大文件时,如果将文件分成合适的大小并使用 Global Dispatch Queue 并列读取的话能提高效率。现今的输入/输出硬件已经可以做到一次使用多个线程更快地并列读取了。能实现这一功能的就是 Dispatch I/O 和 Dispatch Data。

通过Dispatch I/O读写文件时,使用 Global Dispatch Queue 将1个文件按某个大小 read/write。

   dispatch_sync(queue, ^{/* 读取     0 ~  8191 字节*/});
   dispatch_sync(queue, ^{/* 读取  8192 ~ 16383 字节*/});
   dispatch_sync(queue, ^{/* 读取 16384 ~ 24575 字节*/});
   dispatch_sync(queue, ^{/* 读取 24576 ~ 32767 字节*/});

像上面这样,将文件分割为一块一块地进行读取处理。

以下为苹果中使用 Dispatch I/O 和 Dispatch Data 的示例。

   pipe_q = dispatch_queue_create("PipeQ",NULL);
   pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM,fd,pipe_q,^(int err){
      close(fd);
   });
   
   *out_fd = fdpair[i];
   
   dispatch_io_set_low_water(pipe_channel,SIZE_MAX);
   
   dispatch_io_read(pipe_channel,0,SIZE_MAX,pipe_q, ^(bool done,dispatch_data_t pipe data,int err){
      if(err == 0)
        {
          size_t len = dispatch_data_get_size(pipe data);
          if(len > 0)
          {
             const char *bytes = NULL;
             char *encoded;
   
             dispatch_data_t md = dispatch_data_create_map(pipe data,(const void **)&bytes,&len);
             asl_set((aslmsg)merged_msg,ASL_KEY_AUX_DATA,encoded);
             free(encoded);
             _asl_send_message(NULL,merged_msg,-1,NULL);
             asl_msg_release(merged_msg);
             dispatch_release(md);
          }
         }
   
         if(done)
         {
            dispatch_semaphore_signal(sem);
            dispatch_release(pipe_channel);
            dispatch_release(pipe_q);
         }
   });

dispatch_io_create函数生成 Dispatch I/O,并指定发生错误时用来执行处理的 Block,以及执行该 Block的 Dispatch Queue。dispatch_io_set_low_water函数设定一次读取的大小(分割大小),dispatch_io_read函数使用 Global Dispatch Queue 开始并列读取。每当各个分割的文件快读取结束时,将含有文件块数据的 Dispatch Data 传递给dispatch_io_read函数指定的读取结束时回调用的 Block。回调用的 Block 分析传递过来的 Dispatch Data 并进行结合处理。
如果想提高文件读取速度,可以尝试使用Dispatch I/O。

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

推荐阅读更多精彩内容