《Objective-C 高级编程》GCD 笔记摘要

GCD 的 API

Dispatch Queue

串行(Serial):任务按顺序执行,一次只能执行一个任务,只有当前任务执行完毕后,才能执行下一个任务。

并发(Concurrent):任务的执行不需要等待上一个任务执行完后再执行。

并发(Concurrent)和并行(Parallel)的区别

简单点说就是:并发是指同一时间段内能够处理多个任务,如果在单核 CPU 上,是交替地执行任务;并行是指同一时刻处理多个任务。并行需要多核 CPU 的支持,是并发的一个子集。

如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。

在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

引用来自知乎一位用户

dispatch_set_target_queue

作用一:可以改变原队列的优先级,使之与目标队列的优先级一致。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lincoln.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"myConcurrentQueue");
    });
dispatch_queue_t highGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 第一个参数是源队列,第二个参数时目标队列
dispatch_set_target_queue(concurrentQueue, highGlobalQueue);

dispatch_queue_create 生成的队列,不管是串行队列还是并发队列,都使用 Global Queue 的默认优先级。

m

作用二:设置队列的层级体系。比如让多个串行队列和并发队列统一在同一个串行队列中串行执行。

dispatch_queue_t targetQueue = dispatch_queue_create("com.lincoln.targetQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t firstQueue = dispatch_queue_create("com.lincoln.firstQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t secondQueue = dispatch_queue_create("com.lincoln.secondQueue", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_set_target_queue(firstQueue, targetQueue);
dispatch_set_target_queue(secondQueue, targetQueue);

dispatch_async(firstQueue, ^{
    NSLog(@"firstQueue");
});
dispatch_async(secondQueue, ^{
    NSLog(@"secondQueue1");
});
dispatch_async(secondQueue, ^{
    NSLog(@"secondQueue2");
});

/* 打印结果 
firstQueue
secondQueue1
secondQueue2
*/

dispatch_after

作用:在指定时间后追加任务(不是执行任务)到 Dispatch Queue。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"do work");
});

Dispatch Group

作用:有时候希望在一个队列中的任务全部执行完毕后,获得结束通知,就可以采用 Dispatch Group。

使用 dispatch_group_notify 获取结束的通知

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// 追加 Block 到指定的 queue 中,Block 持有 group 的引用。
dispatch_group_async(group, queue, ^{
    NSLog(@"任务一");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"任务二");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"任务三");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"3个任务都结束后,在主线程执行相关操作");
});

使用 dispatch_group_wait 单纯等待全部任务的结束情况,没有结束通知的获取

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
    // 追加 Block 到指定的 queue 中,Block 持有 group 的引用。
dispatch_group_async(group, queue, ^{
    NSLog(@"任务一");
    [NSThread sleepForTimeInterval:2];
});
dispatch_group_async(group, queue, ^{
    NSLog(@"任务二");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"任务三");
});
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
/* dispatch_group_wait 仅等待全部处理执行结束。
 第二个参数是 dispatch_time_t 类型, 用来标识在规定时间内 group 中的任务是否全部完成。
 完成返回 0,超时返回 1。
 如要一直等待任务完成,第二个参数可以传入 DISPATCH_TIME_FOREVER,此时返回值恒为 0。
 如果第二个参数传入 DISPATCH_TIME_NOW,则返回值恒为 1。
 */
long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
if (result == 0) {
    NSLog(@"任务在规定时间内全部处理完成");
} else {
    NSLog(@"任务超时");
}

ps:在一般情况下,推荐用 dispatch_group_notify 函数追加结束处理到所需的队列(一般是主队列)中。

dispatch_barrier_async

作用:队列中的任务执行到一半时,进行拦截,执行需要的操作,然后继续执行剩下的任务。

dispatch_queue_t queue = dispatch_queue_create("com.lincoln.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"读取1");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"读取2");
});
dispatch_async(queue, ^{
    NSLog(@"读取3");
});
// 当执行完上面队列中的任务后,进行拦截,接着再执行下面剩余的任务。
dispatch_barrier_async(queue, ^{
    NSLog(@"进行数据写入");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"读取4");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"读取5");
});
dispatch_async(queue, ^{
    NSLog(@"读取6");
});
dispatch_async(queue, ^{
    NSLog(@"读取7");
});

dispatch_sync

作用:同步执行任务。这很容易造成死锁,需要谨慎。现举 2 个 死锁的例子。

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
    NSLog(@"Hello?");
});

/* 上面的代码会造成死锁!!!原因如下:
 现在主队列上有2个任务:
 任务1:执行 dispatch_sync 函数
 任务2:将 Block 添加到主队列
 因为是同步,所以先执行任务 1,再执行任务 2。dispatch_sync 函数执行完毕的标志是成功将 Block 添加到了主队列。也就是说,任务1 执行的任务和任务 2 是同一个任务。那么这就造成了相互等待的问题,也就是死锁的问题。
*/
dispatch_queue_t serialQueue = dispatch_queue_create("com.lincoln.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{    
  dispatch_sync(serialQueue, ^{
      NSLog(@"Hello?");
  });
});

/*
 这个和上面这个类似。上面这个是在主线程发生死锁,这个是在串行队列新开的线程中发生死锁。
*/

dispatch_apply

作用:按指定的次数将指定的 Block 追加到指定的队列中。(会阻塞线程)

//  dispatch_apply 函数会等待任务全部执行完毕。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"index: %zu", index);
});

NSLog(@"done");

// 由于 dispatch_apply 函数也与 dispatch_sync 函数相同,会等待处理执行结束,因此推荐在  dispatch_async 中非同步的执行 dispatch_apply 函数。
NSArray *array = @[@"a", @"b", @"c"];
dispatch_async(queue, ^{
    dispatch_apply(array.count, queue, ^(size_t index) {
        NSLog(@"%zu: %@", index, array[index]);
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"返回主队列");
    });
});

dispatch_suspend / dispatch_resume

作用:挂起和恢复队列。需要注意的是,正在队列中执行的任务是不受影响的。

Dispatch Semaphore

Dispatch Semaphore 是持有计数的信号,该计数是多线程中的技术类型信号。当计数值大于等于 1 时,可以执行排他操作,然后将计数减 1,当计数等于 0 时,不能执行排他操作。这是一种比 Dispatch Serial 和 dispatch_barrier_async 更细粒度的控制。

  1. dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); 创建了一个信号。
  2. long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 等待信号的计数大于等于1,第二个参数是等待时间,如果超时或者计数等于 0,则返回值 result 等于 1。如果计数值大于等于1,并且未超时,则返回值 result 等于 0,执行排他处理,并且计数值减 1。
  3. dispatch_semaphore_signal(semaphore); 使计数值加 1。

下面函数可能很可能运行报错

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
    // 很可能导致多个线程同时访问 array
    dispatch_async(queue, ^{
        [array addObject:@(i)];
    });
}

通过 Dispatch Semaphore 解决

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [NSMutableArray new];
// Dispatch Semaphore 初始化计数值为 1。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
    dispatch_async(queue, ^{
        // 一直等待,直到 Dispatch Semaphore 计数值大于等于 1。保证只有 1 条线程可访问 array 对象。
        long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if (result == 0) {
          // 将 Dispatch Semaphore 计数值减 1,然后操作 array
            [array addObject:@(i)];
        }
        // 将 Dispatch Semaphore 计数值加 1。
        dispatch_semaphore_signal(semaphore);
    });
}

ps:一般 Dispatch Semaphore 是在子线程环境下使用。

dispatch_once

作用:只执行一次,多用于创建单例。

- (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static Person *p;
    dispatch_once(&onceToken, ^{
        p = [[Person alloc] init];
    });
    return p;
}

Dispatch I/O

作用:对数据进行读写操作。可以一次使用多个线程并列地将一个较大的文件分割为一块一块地进行读取处理。分割读取的数据通过使用 Dispatch Data 可更为简单地进行结合和分割。

ps:因为对 Dispatch I/O 相关的 API 不太了解,故此处不提供 demo,读者可以上网查找。

GCD 的实现

Dispatch Queue

GCD 的实现需要在系统级实现,需要调用 XNU 系统内核,所以开发者编写管理线程的代码,在性能方面不可能胜过 XNU 内核级所实现的 GCD。

用于实现 Dispatch Queue 而使用的软件组件

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

开发者所使用的 GCD 的API 都包含在 libdispatch 库中,这些 API 都是 C 语言函数。Dispatch Queue 通过结构体和链表,被实现为 FIFO 队列。

添加到 Dispatch Queue 中的 Block 并不直接加入 FIFO 队列,而是先加入 Dispatch Continuation 这一 dispatch_continuation_t 类型结构体中,然后再加入 FIFO 队列。该 Dispatch Continuation 用于记忆 Block 所属的 Dispatch Group 和其他一些信息,相当于一般常说的执行上下文。

Global Dispatch Queue 有 8 中优先级,分别为:

  • High Priority
  • Default Priority
  • Low Priority
  • Background Priority
  • High Overcommit Priority
  • Default Overcommit Priority
  • Low Overcommit Priority
  • Background Overcommit Priority

优先级中有 Overcommit 的 Global Dispatch Queue 使用在 Serial Dispatch Queue 中。带有 Overcommit 的优先级,不管系统状态如何,都会强制生成线程的 Dispatch Queue。

这 8 种 Global Dispatch Queue 各使用 1 个 pthread_workqueue。GCD 初始化时,使用 pthread_workqueue_create_np 函数生成 pthread_workqueue。

XNU 内核持有 4 种 workqueue,分别为:

  • WORKQUEUE_HIGH_PRIOQUEUE
  • WORKQUEUE_DEFAULT_PRIOQUEUE
  • WORKQUEUE_LOW_PRIOQUEUE
  • WORKQUEUE_BG_PRIOQUEUE

以上 4 中优先级的 workqueue 与 Global Dispatch Queue 的 4 种优先级相同。

Dispatch Queue 中执行 Block 的过程

  1. 当在 Global Dispatch Queue 中执行 Block 时,libdispatch 从 Global Dispatch Queue 自身的 FIFO 队列中取出 Dispatch Continuation,调用 pthread_workqueue_additem_np 函数。
  2. 将 Global Dispatch Queue 自身、符合其优先级的 workqueue 信息以及为执行 Dispatch Continuation 的回调函数等传递给参数。
  3. pthread_workqueue_additem_np 函数使用 workq_kernreturn 系统调用,通知 workqueue 增加应当执行的项目。
  4. 根据该通知,XNU 内核基于系统状态判断是否要生成线程。如果是 Overcommit 优先级的 Global Dispatch Queue,workqueue 则始终生成线程。
  5. workqueue 的线程执行 pthread_workqueue 函数,该函数调用 libdispatch 的回调函数。
  6. 在该回调函数中执行加入到 Dispatch Continuation 的 Block。
  7. Block 执行结束后,进行通知 Dispatch Group 结束、释放 Dispatch Continuation 等处理。
  8. 开始准备执行加入到 Global Dispatch Queue 中的下一个 Block。

Dispatch Source

Dispatch Source 是 BSD 系内核惯有功能 kqueue 的包装。kqueue 是在 XNU 内核中发生各种事件时,对各种事件进行处理的技术。其 CPU 负载非常小,尽量不占用资源。

kqueue 可以说是处理 XNU 内核中各种事件的方法中最优秀的一种。

Dispatch Source 的种类

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加,属于自定义事件
DISPATCH_SOURCE_TYPE_DATA_OR 属于自定义事件,用法同上面的类型一样
DISPATCH_SOURCE_TYPE_MACH_SEND Mach端口发送事件
DISPATCH_SOURCE_TYPE_MACH_RECV Mach端口接收事件
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 读取文件事件
DISPATCH_SOURCE_TYPE_WRITE 写入文件事件
DISPATCH_SOURCE_TYPE_VNODE 文件属性更改事件
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号事件
DISPATCH_SOURCE_TYPE_TIMER 定时器事件
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力事件

source_type_read

- (void)source_type_read {
  __block size_t total = 0;
  // 要读取的字节数
  size_t size = 1024 * 1024;
  char *buff = (char *)malloc(size);

  NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
  NSString *fileName = [filePath stringByAppendingPathComponent:@"markdown语法.md"];
  int fd = open([fileName UTF8String], O_WRONLY | O_CREAT | O_TRUNC, (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
  NSLog(@"write fd: %d", fd);
  if (fd == -1) return;

  // 设定为异步映像
  fcntl(fd, F_SETFL, O_NONBLOCK);
  // 获取用于追加事件处理的 Global Dispatch Queue
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  // 基于 READ 事件的 Dispatch Source
  dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
  // 指定发生 READ 事件时执行的处理
  dispatch_source_set_event_handler(source, ^{
     // 获取可读写的字节数
      size_t available = dispatch_source_get_data(source);
      // 从映像中读取
      ssize_t length = read(fd, buff, available);
      // 发生错误时取消 Dispatch Source
      if (length < 0) {
          dispatch_source_cancel(source);
      }
      total += length;
      if (total == size) {
          // buff 的处理
          NSLog(@"对 buff 进行处理");
          // 处理结束后,取消 Dispatch Source
          dispatch_source_cancel(source);
      }
  });
  // 指定取消 Dispatch Source 时的处理
  dispatch_source_set_cancel_handler(source, ^{
      free(buff);
      close(fd);
  });
  // 启动 Dispatch Source
  dispatch_resume(source);
}

source_type_timer

- (void)source_type_timer {
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    // 将定时器设置为 15 秒后,不重复,允许延迟 1 秒。
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15ull * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC);
    // 指定定时器指定时间内执行的处理
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"wake up!");
        // 取消 Dispatch Source
        dispatch_source_cancel(timer);
    });
    //指定取消 Dispatch Source 时的处理
    dispatch_source_set_cancel_handler(timer, ^{
        NSLog(@"cancelled");
    });
    // 启动 Dispatch Source
    dispatch_resume(timer);
}

参考

Dispatch Source学习
细说GCD
Coucurrency Programming Guide

demo 地址:Github

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

推荐阅读更多精彩内容