关于GCD的那些事儿

一、遇到的问题

在项目中经常会遇到这样的问题,一个页面由于内容繁多,结构复杂,后台写了5个接口进行支持,这5个接口互相又没有什么影响,也没什么顺序,但是就是需要把这5个接口的数据全都拿到之后组合一下然后刷新页面。

今天我们主要来谈一下怎么更好的用第的方法来实现上述需求,在iOS里面GCD技术正好可以解决这种问题,当然也有其他方式,只是GCD用起来代码更简洁,实现更优雅。

1.1 解决方法

说到用GCD来解决,其实也有很多解决方法,我们先来说一种解决方法

//获取一下系统提供的全局队列
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, ^{
        
    //创建一个计数信号Dispatch Semaphore 初始值设为0
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    //发起第一个网络请求
    [Network GET:@"api" completion:^(id  _Nonnull result) {
        
        NSLog(@"网络请求1,执行完成");
        
        //将Dispatch Semaphore计数信号值加1 这个要写到网络请求的回调里,无论成功失败。
        dispatch_semaphore_signal(semaphore);

    }];
    
    //一直等待,直到Dispatch Semaphore的计数值达到大于等于1
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

.
.
. /* 中间三个请求是一模一样的,所以就先省略。 */
.
.

dispatch_group_async(group, queue, ^{
        
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    //发起第5个网络请求 
    [Network GET:@"api" completion:^(id  _Nonnull result) {
        NSLog(@"网络请求5,执行完成");
        dispatch_semaphore_signal(semaphore);
    }];
        
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

//全部执行结束
dispatch_group_notify(group, queue, ^{
    NSLog(@"请求全部执行结束");
});

日志输出

2019-11-19 20:04:36.258363+0800 GCD-demo[8104:784197] 网络请求3,执行完成
2019-11-19 20:04:36.258383+0800 GCD-demo[8104:784200] 网络请求5,执行完成
2019-11-19 20:04:36.258418+0800 GCD-demo[8104:784198] 网络请求1,执行完成
2019-11-19 20:04:36.258425+0800 GCD-demo[8104:784196] 网络请求2,执行完成
2019-11-19 20:04:36.258429+0800 GCD-demo[8104:784199] 网络请求4,执行完成
2019-11-19 20:04:36.258642+0800 GCD-demo[8104:784163] 请求全部执行结束

以上就是GCD异步并发实现五个请求的一种方法。

下面我们解释一下这种方法为什么要这么写,实现原理,以及各个GCD中函数的含义及用法。

二、GCD的API

2.1 什么是Dispatch Queue

Dispatch Queue就是执行处理的等待队列。程序猿们通过dispatch_async等一些函数API在Block中写一些自己想执行的代码,并追加的Dispatch Queue中。然后Dispatch Queue按照追加的顺序(学术用语先进先出FIFO)执行处理。

dispatch_async(queue, ^
    //想要执行的处理
});

执行处理时存在两种Dispatch Queue,一种是串行队列Serial Dispatch Queue,一种是并行队列Concurrent Dispatch Queue。

2.2. 如何得到一个Dispatch Queue

得到Dispatch Queue有两种方法,一种是通过GCD的API生成,另一种是获取系统标准提供的Dispatch Queue。

2.2.1 通过GCD的API生成

通过dispatch_queue_create函数可生成Dispatch Queue。

  • 生成一个Serial Dispatch串行队列
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("mySerialDispacthQueue", NULL);

该函数的第一个参数指定串行队列的名称,例如上面的例子,Dispatch Queue的名称推荐使用应用程序的ID这种逆序全程域名,这样命名简单易懂,方便调试,。当然你也可以设为NULL,但调试时候不是那么方便。第二个参数指定为NULL,当然你也可以设DISPATCH_QUEUE_SERIAL不过没什么意义,本身DISPATCH_QUEUE_SERIAL就是NULL,不信看API啊。

  • 生成一个Concurrent Dispatch Queue 并行队列
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

生成并行队列的时候第一个参数同上,第二个参数必须写DISPATCH_QUEUE_CONCURRENT。

2.2.2 获取系统标准提供的Dispatch Queue

实际上呢,也不用特意去生成Dispatch Queue,系统会给我们提供几个,比如Main Dispatch Queue和Global Dispatch Queue。

Main Dispatch Queue是在主线程执行的队列,因为主线程只有1个,所以Main Dispatch Queue是Serial Dispatch Queue串行队列。

而Global Dispatch Queue是所有应用程序都能使用的Concurrent Dispatch Queue,一般没必要通过函数dispatch_queue_create逐个生成Concurrent Dispatch Queue。只要获取一下系统提供的Global Dispatch Queue使用即可。另外呢Global Dispatch Queue有4个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。

系统提供的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 执行优先级:后台

各种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);

2.2.3 GCD同步和异步方法

  • 同步操作
dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>, <#^(void)block#>)
  • 异步操作
dispatch_async(<#dispatch_queue_t  _Nonnull queue#>, <#^(void)block#>)

2.2.4 串行、并行队列和同步、异步结合分析

dispatch_sync dispatch_async
串行队列 不开启新线程,在当前线程按序执行任务 开启一条新的线程,新创建的线程中任务是串行的
主队列 不开启新线程,在主线线程按序执行任务;如果在主线程使用这种组合会死锁 不开启新线程,在主线线程按序执行任务
并发 / 全局并发队列 不开启新线程,在当前线程按序执行任务 可以同时开启多条线程,任务是并发执行的,具体开启多少条线程有GCD自动根据CPU情况决定

2.3 Dispatch Group

Dispatch Group就是我们刚开始抛出的问题解决方案中的主要函数,其作用就是把并发的几个队列Dispatch Queue任务加到组里,然后调用dispatch_group_notify或者dispatch_group_wait监听结束。

//获取一下系统提供的全局队列
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(@"第1个任务");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"第2个任务");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"第3个任务");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"第4个任务");
});

//全部执行结束
dispatch_group_notify(group, queue, ^{
    NSLog(@"请求全部执行结束");
});    

输出日志

2019-11-19 19:50:12.347291+0800 GCD-demo[7994:761684] 第2个任务
2019-11-19 19:50:12.347291+0800 GCD-demo[7994:761686] 第3个任务
2019-11-19 19:50:12.347292+0800 GCD-demo[7994:761683] 第1个任务
2019-11-19 19:50:12.347317+0800 GCD-demo[7994:761685] 第4个任务
2019-11-19 19:50:12.347453+0800 GCD-demo[7994:761685] 请求全部执行结束

因为想Global Dispatch Queue即Concurrent Dispatch Queue追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行时会发生变化,但是执行结果一定是最后输出的。

无论想什么样的Dispatch Queue中追加处理,使用Dispatch Group都可以监视这些处理执行的结束。一旦检测到所有的处理执行结束,就可将结束的处理追加到Dispatch Queue中。这就是使用Dispatch Group的原因所在。

下面简单解释一下上面代码的含义,首先dispatch_group_create函数生成dispatch_group_t类型的Dispatch Group。然后调用dispatch_group_async函数追加处理,最后用dispatch_group_notify监视处理执行的结束。dispatch_group_async与Dispatch Queue的dispatch_async函数作用相同,都是将Block的代码追加到Dispatch Queue中。不同点是dispatch_group_async需要将指定的Dispatch Group作为第一个参数。dispatch_group_notify函数的第一个参数是指定要监视的Dispatch Group。在追加到该Dispatch Group的全部处理执行结束的时候,将第三个参数的Block追加到第二个参数的Dispatch Queue中。

当然,第二个参数也不是必须是dispatch_group_async函数中的Dispatch Queue,可以是任意的Dispatch Queue。Dispatch Group还有一个监视结束的函数dispatch_group_wait,这个函数需要两个参数,一个是要监视的Dispatch Group,另一个是等待时间。

2.4 Dispatch Semaphore

GCD函数Dispatch Semaphore信号量。

//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

//等待降低信号量,等待时间为DISPATCH_TIME_FOREVER时即永久,直到信号量大于或等于1,函数会对信号量的值进行减1操作,然后返回
dispatch_semaphore_wait(信号量,等待时间)

//信号量加1
dispatch_semaphore_signal(信号量)

生成Dispatch Semaphore需要一个初始值,这个初始值就是信号量数值。

2.4.1 dispatch_semaphore_t创建多线程网络同步请求

我们用一开始文章开头抛出的问题来说一下这个Dispatch Semaphore的用法。

dispatch_group_async(group, queue, ^{

    //创建一个Dispatch Semaphore初始值为0    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    //发起网络请求
    [Network GET:@"api" completion:^(id  _Nonnull result) {
        NSLog(@"网络请求完成");
        //信号量加1
        dispatch_semaphore_signal(semaphore);
    }];

    //等待Dispatch Semaphore的计数值达到大于或者等于1
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

首先我们先想一下,如果这个例子不写Dispatch Semaphore会怎么样。我们来看一下输出日志。

2019-11-19 20:53:40.277849+0800 GCD-demo[8633:900152] 请求全部执行结束
2019-11-19 20:53:50.277986+0800 GCD-demo[8633:900129] 网络请求1,执行完成
2019-11-19 20:53:50.277993+0800 GCD-demo[8633:900127] 网络请求2,执行完成
2019-11-19 20:53:50.277996+0800 GCD-demo[8633:900151] 网络请求5,执行完成
2019-11-19 20:53:50.278000+0800 GCD-demo[8633:900130] 网络请求4,执行完成
2019-11-19 20:53:50.278008+0800 GCD-demo[8633:900128] 网络请求3,执行完成

出现上面的情况,dispatch_group_notify监视到整个Dispatch Group已经结束,然而,网络请求还没有完成。因此,我们需要一个信号,告诉dispatch_group_async函数,什么时候才是真正的完成的执行处理。

Dispatch Semaphore正好就是干这个事情的,我们先声明一个Dispatch Semaphore,初始值就是0,然后执行dispatch_semaphore_wait函数,dispatch_semaphore_wait函数需要两个入参,分别是等待的Dispatch Semaphore还有等待时间。Dispatch Semaphore在设定的等待时间内检测到信号量小于1,就会一直等待,直到网络请求完成,dispatch_semaphore_signal函数把信号量的值加1,使其Dispatch Semaphore的计数值达到大于等于1,或者超时。达到其中一个条件就会告诉dispatch_group_async函数此次执行处理完成。

2.4.2 dispatch_semaphore_t设置最大并发数

//获取一下系统提供的全局队列DISPATCH_QUEUE_CONCURRENT 生成一个并发队列
dispatch_queue_t queue = dispatch_queue_create("myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

//创建一个Dispatch Semaphore初始值为2
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);

//新建一个组
dispatch_group_t group = dispatch_group_create();
 
for (int i = 0; i < 10; i++) {
    dispatch_group_async(group, queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            // 线程操作区域 最多有两个线程在此工作
            
            dispatch_semaphore_signal(semaphore);
    });
}

//全部执行结束
dispatch_group_notify(group, queue, ^{
    NSLog(@"全部执行结束");
});

通过设置dispatch_semaphore_create参数为2,利用Dispatch Semaphore的特性dispatch_semaphore_waitdispatch_semaphore_signal之间最多有两个线程在工作,从而达到设置队列的最大并发数的目的。

2.5 Dispatch Barrier

栅栏函数,等待在Dispatch Barrier前面插入队列的任务先执行完,再执行后面的任务。

// queue: 将barrier添加到的队列 , block: barrier执行的任务
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

2.5.1 dispatch_barrier_sync

dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们。

// 创建Concurrent Dispatch Queue 并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

// 向队列中添加任务
dispatch_async(concurrentQueue, ^{
   NSLog(@"第1个任务");
});

dispatch_async(concurrentQueue, ^{
   NSLog(@"第2个任务");
});

// dispatch_barrier_sync 添加任务
dispatch_barrier_sync(concurrentQueue, ^{
    sleep(1);
   NSLog(@"Dispatch Barrier任务");
});

NSLog(@"主线程任务1");

dispatch_async(concurrentQueue, ^{
    NSLog(@"第3个任务");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"第4个任务");
});

NSLog(@"主线程任务2");

输出日志

2019-11-20 17:58:13.737806+0800 GCD-demo[51368:20064429] 第1个任务
2019-11-20 17:58:13.737831+0800 GCD-demo[51368:20064430] 第2个任务
2019-11-20 17:58:14.739234+0800 GCD-demo[51368:20064366] Dispatch Barrier任务
2019-11-20 17:58:14.739529+0800 GCD-demo[51368:20064366] 主线程任务1
2019-11-20 17:58:14.739739+0800 GCD-demo[51368:20064366] 主线程任务2
2019-11-20 17:58:14.739773+0800 GCD-demo[51368:20064430] 第3个任务
2019-11-20 17:58:14.739778+0800 GCD-demo[51368:20064429] 第4个任务

由此可见,dispatch_barrier_sync必须等待自己任务结束,才会把后续任务添加到队列,然后执行它们。

2.5.2 dispatch_barrier_async

dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。

// 创建Concurrent Dispatch Queue 并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

// 向队列中添加任务
dispatch_async(concurrentQueue, ^{
   NSLog(@"第1个任务");
});

dispatch_async(concurrentQueue, ^{
   NSLog(@"第2个任务");
});

// dispatch_barrier_async 添加任务
dispatch_barrier_async(concurrentQueue, ^{
    sleep(1);
   NSLog(@"Dispatch Barrier任务");
});

NSLog(@"主线程任务1");

dispatch_async(concurrentQueue, ^{
    NSLog(@"第3个任务");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"第4个任务");
});

NSLog(@"主线程任务2");

输出日志

2019-11-20 17:54:05.535233+0800 GCD-demo[51346:20058026] 主线程任务1
2019-11-20 17:54:05.535251+0800 GCD-demo[51346:20058062] 第1个任务
2019-11-20 17:54:05.535263+0800 GCD-demo[51346:20058060] 第2个任务
2019-11-20 17:54:05.535402+0800 GCD-demo[51346:20058026] 主线程任务2
2019-11-20 17:54:06.539084+0800 GCD-demo[51346:20058060] Dispatch Barrier任务
2019-11-20 17:54:06.539451+0800 GCD-demo[51346:20058062] 第4个任务
2019-11-20 17:54:06.539438+0800 GCD-demo[51346:20058060] 第3个任务

由此可见,dispatch_barrier_async不会拦截后续任务加载到队列,但后续任务必须等待dispatch_barrier_async任务执行结束,后续任务才可执行

2.6 Dispatch Source

GCD中除了主要的 Dispatch Queue 外,还有不太引人注目的 Dispatch Source 。使用 Dispatch Source而不使用 Dispatch Queue唯一原因就是利用联结的优势。

联结的大致流程:在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。

这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件。

简单地说,这种事件是由你调用 dispatch_source_merge_data 函数来向自己发出的信号。

2.6.1 创建一个Dispatch Source

// API
dispatch_source_t source = dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue)

// 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

参数:

参数 意义
type dispatch源可处理的事件
handle 可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID
mask 可以理解为描述,提供更详细的描述,让它知道具体要监听什么
queue 自定义源需要的一个队列,用来处理所有的响应句柄(block)

Dispatch Source 可处理的所有事件。如下表所示:

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 监测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

自定义源也需要一个队列,用来处理所有的响应句柄(block)。那么岂不是有两个队列了?没错,至于 Dispatch Queue 这个队列的线程执行与 Dispatch Source这个队列的线程执行的关系,下文会详细介绍。

2.6.2 处理Dispatch Source的暂停与恢复操作

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。

在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。

dispatch_suspend(queue);

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

分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。因为忘记恢复分派源的状态而产生bug是常见的事儿。恢复的方法是调用 dispatch_resume :

dispatch_resume (source);

思考下NSLog的打印顺序为什么会是这样?

dispatch_queue_t queue1 = dispatch_queue_create("com.serial.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.serial.queue2", DISPATCH_QUEUE_SERIAL);
dispatch_group_t group = dispatch_group_create();

dispatch_async(queue1, ^{
   NSLog(@"任务 1 : queue 1...");
   sleep(1);
   NSLog(@"✅完成任务 1");
});

dispatch_async(queue2, ^{
   NSLog(@"任务 1 : queue 2...");
   sleep(1);
   NSLog(@"✅完成任务 2");
});

dispatch_group_async(group, queue1, ^{
   NSLog(@"🚫正在暂停 1");
   dispatch_suspend(queue1);
});
dispatch_group_async(group, queue2, ^{
   NSLog(@"🚫正在暂停 2");
   dispatch_suspend(queue2);
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"=======等待两个queue完成, 再往下进行...");
dispatch_async(queue1, ^{
   NSLog(@"任务 2 : queue 1");
});
dispatch_async(queue2, ^{
   NSLog(@"任务 2 : queue 2");
});
NSLog(@"🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️");

dispatch_resume(queue1);
dispatch_resume(queue2);

打印日志:

2020-04-17 15:41:45.049437+0800 多线程-demo[54147:1097252] 任务 1 : queue 2...
2020-04-17 15:41:45.049437+0800 多线程-demo[54147:1097254] 任务 1 : queue 1...
2020-04-17 15:41:46.053559+0800 多线程-demo[54147:1097252] ✅完成任务 2
2020-04-17 15:41:46.053569+0800 多线程-demo[54147:1097254] ✅完成任务 1
2020-04-17 15:41:46.053820+0800 多线程-demo[54147:1097252] 🚫正在暂停 2
2020-04-17 15:41:46.053830+0800 多线程-demo[54147:1097254] 🚫正在暂停 1
2020-04-17 15:41:46.053986+0800 多线程-demo[54147:1096499] =======等待两个queue完成, 再往下进行...
2020-04-17 15:41:46.054112+0800 多线程-demo[54147:1096499] 🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️
2020-04-17 15:41:46.054279+0800 多线程-demo[54147:1097252] 任务 2 : queue 1
2020-04-17 15:41:46.054282+0800 多线程-demo[54147:1097254] 任务 2 : queue 2

2.6.3 创建Dispatch Source的事件处理

创建源后,需要提供相应的处理方法。当源生效时会分派注册处理方法;当事件发生时会分派事件处理方法;当源被取消时会分派取消处理方法。自定义源通常只需要一个事件处理方法,可以像这样创建:

// 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(_source, ^{
    //当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
    NSUInteger value = dispatch_source_get_data(self->_source);
    totalComplete += value;
    NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
    NSLog(@"🔵线程号:%@", [NSThread currentThread]);
});

// 分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
dispatch_resume(_source);
NSLog(@"✅恢复Dispatch Source(分派源)");

// 恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_source_merge_data(self->_source, 1);
        NSLog(@"♻️线程号:%@", [NSThread currentThread]);
        usleep(20000);//0.02秒
    }
});

打印日志:

2020-04-17 15:45:39.260746+0800 多线程-demo[54302:1102537] ✅恢复Dispatch Source(分派源)
2020-04-17 15:45:39.260746+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
2020-04-17 15:45:39.275313+0800 多线程-demo[54302:1102090] 进度:0.01
2020-04-17 15:45:39.275452+0800 多线程-demo[54302:1102090] 🔵线程号:<NSThread: 0x60000275ae40>{number = 1, name = main}
2020-04-17 15:45:39.281218+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
2020-04-17 15:45:39.287779+0800 多线程-demo[54302:1102090] 进度:0.02
2020-04-17 15:45:39.287941+0800 多线程-demo[54302:1102090] 🔵线程号:<NSThread: 0x60000275ae40>{number = 1, name = main}
2020-04-17 15:45:39.301559+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
/*================省略中间====================*/
2020-04-17 15:45:41.553927+0800 多线程-demo[54302:1102090] 进度:0.98
2020-04-17 15:45:41.553975+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
2020-04-17 15:45:41.554374+0800 多线程-demo[54302:1102090] 🔵线程号:<NSThread: 0x60000275ae40>{number = 1, name = main}
2020-04-17 15:45:41.577753+0800 多线程-demo[54302:1102090] 进度:0.99
2020-04-17 15:45:41.577754+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
2020-04-17 15:45:41.578062+0800 多线程-demo[54302:1102090] 🔵线程号:<NSThread: 0x60000275ae40>{number = 1, name = main}
2020-04-17 15:45:41.603169+0800 多线程-demo[54302:1102537] ♻️线程号:<NSThread: 0x60000274d880>{number = 5, name = (null)}
2020-04-17 15:45:41.603189+0800 多线程-demo[54302:1102090] 进度:1
2020-04-17 15:45:41.603444+0800 多线程-demo[54302:1102090] 🔵线程号:<NSThread: 0x60000275ae40>{number = 1, name = main}

耗时:2.343

在同一时间,只有一个处理方法块的实例被分派。如果这个处理方法还没有执行完毕,另一个事件就发生了,事件会以指定方式(ADD或者OR)进行累积。通过合并事件的方式,系统即使在高负 载情况下也能正常工作。当处理事件件被最终执行时,计算后的数据可以通过 dispatch_source_get_data 来获取。这个数据的值在每次响应事件执行后会被重置,所以上面例子中 totalComplete 的值是最终累积的值。

恢复源后,就可以像下面的代码片段这样,通过 dispatch_source_merge_data 向分派源发送事件。在每次循环中执行加1操作。也可以传递已处理记录的数目或已写入的字节数。在任何线程中都可以调用 dispatch_source_merge_data 。需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。

2.6.3 Dispatch Source能通过合并事件的方式确保在高负载下正常工作

上部分代码还可以进行如下优化:

// 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定 global Dispatch Queue 为追加处理的Dispatch Queue
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(_source, ^{
    //当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
    NSUInteger value = dispatch_source_get_data(self->_source);
    totalComplete += value;
    NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
    NSLog(@"🔵线程号:%@", [NSThread currentThread]);
});

// 分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
dispatch_resume(_source);
NSLog(@"✅恢复Dispatch Source(分派源)");

//恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSUInteger index = 0; index < 100; index++) {
   dispatch_async(queue, ^{
       dispatch_source_merge_data(self->_source, 1);
       NSLog(@"♻️线程号:%@", [NSThread currentThread]);
       usleep(20000);//0.02秒
   });
}

打印日志:

2020-04-17 16:06:32.071589+0800 多线程-demo[54925:1125521] ✅恢复Dispatch Source(分派源)
2020-04-17 16:06:32.071854+0800 多线程-demo[54925:1126037] ♻️线程号:<NSThread: 0x600003dc8680>{number = 7, name = (null)}
2020-04-17 16:06:32.071854+0800 多线程-demo[54925:1126034] ♻️线程号:<NSThread: 0x600003de0f80>{number = 6, name = (null)}
2020-04-17 16:06:32.071858+0800 多线程-demo[54925:1126040] ♻️线程号:<NSThread: 0x600003dee440>{number = 3, name = (null)}
2020-04-17 16:06:32.071859+0800 多线程-demo[54925:1126035] ♻️线程号:<NSThread: 0x600003dd5680>{number = 5, name = (null)}
2020-04-17 16:06:32.071892+0800 多线程-demo[54925:1126039] ♻️线程号:<NSThread: 0x600003dd54c0>{number = 8, name = (null)}
2020-04-17 16:06:32.071976+0800 多线程-demo[54925:1126043] ♻️线程号:<NSThread: 0x600003dcf180>{number = 9, name = (null)}
2020-04-17 16:06:32.071994+0800 多线程-demo[54925:1126044] ♻️线程号:<NSThread: 0x600003de5800>{number = 10, name = (null)}
/*================省略中间====================*/
2020-04-17 16:06:32.288005+0800 多线程-demo[54925:1126059] ♻️线程号:<NSThread: 0x600003dd53c0>{number = 25, name = (null)}
2020-04-17 16:06:32.288000+0800 多线程-demo[54925:1126036] ♻️线程号:<NSThread: 0x600003db0580>{number = 4, name = (null)}
2020-04-17 16:06:32.288019+0800 多线程-demo[54925:1126044] ♻️线程号:<NSThread: 0x600003de5800>{number = 10, name = (null)}
2020-04-17 16:06:32.287870+0800 多线程-demo[54925:1126053] ♻️线程号:<NSThread: 0x600003dd5440>{number = 19, name = (null)}
2020-04-17 16:06:32.288022+0800 多线程-demo[54925:1126099] ♻️线程号:<NSThread: 0x600003de5d80>{number = 65, name = (null)}
2020-04-17 16:06:32.287878+0800 多线程-demo[54925:1126068] ♻️线程号:<NSThread: 0x600003dc1880>{number = 34, name = (null)}
2020-04-17 16:06:32.288844+0800 多线程-demo[54925:1126087] 进度:1
2020-04-17 16:06:32.287878+0800 多线程-demo[54925:1126067] ♻️线程号:<NSThread: 0x600003dcf540>{number = 33, name = (null)}
2020-04-17 16:06:32.310412+0800 多线程-demo[54925:1126087] 🔵线程号:<NSThread: 0x600003ddb480>{number = 53, name = (null)}

耗时:0.239秒,与之前的2.343秒相比,时间是后者的10倍 ,性能相差很大。

然而上例中也因为并发执行,速度相当快,调用 dispatch_source_merge_data 后所触发的 dispatch_source_set_event_handler 的频率也大大减少,有时只会在结束时触发一次。

如果你细心观察下上例中的打印🔵(小蓝点)♻️(小绿点)个数是不一的,但 totalComplete 的值,或者进度条从0.0到1.0的执行是正常,但是🔵(小蓝点)为什么没有被打印?这是因为:

DispatchSource能通过合并事件的方式确保在高负载下正常工作

在同一时间,只有一个处理 block 的实例被分配,如果这个处理方法还没有执行完毕,另一个事件就发生了,事件会以指定方式(ADD或 OR)进行累积。DispatchSource能通过合并事件(block)的方式确保在高负载下正常工作。当处理事件被最终执行时,计算后的数据可以通过 dispatch_source_get_data 来获取。这个数据的值在每次响应时间执行后会被重置,所以上面的例子中进度条 totalComplete 的值是最终积累的值,而 block 不是每次都执行的,但打印🔵(小蓝点)♻️(小绿点)个数不一。但能确保进度条能从0.0到1.0的正常执行。

2.6.5 Dispatch Source 与 Dispatch Queue 同时实现暂停和恢复

上面的代码是有问题的,它只是一种“假暂停”的状态。for 循环还是要执行100遍,循环的次数并没有因你暂停了派发源而暂停,这在实际开发中是不允许的,因为真正的性能瓶颈永远会是在这里,这样的暂停毫无意义。那么如何让 for 循环随时可以暂停?

实际上 Dispatch Queue 没有“取消”这一概念。一旦将处理追加到 Dispatch Queue 中,就没有方法可将该处理去除,也没有方法可在执行中取消该处理。编程人员要么在处理中导入取消这一概念。

要么放弃取消,或者使用 NSOperationQueue 等其他方法。

Dispatch SourceDispatch Queue 不同,是可以取消的。而且取消时必须执行的处理可指定为回调用的Block形式。

那么如何在处理中导入取消这一概念?代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
    _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
                                                    dispatch_get_main_queue());
    __block NSUInteger totalComplete = 0;
    dispatch_source_set_event_handler(_source, ^{
        // 当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
        NSUInteger value = dispatch_source_get_data(self->_source);
        totalComplete += value;
        NSLog(@"进度:%@", @((CGFloat)totalComplete/LMTotalNumber));
    });
    // 分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
    [self resume];
    
    // 恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
    //为了便于观察,将_queue做成“串行队列”
    _queue = dispatch_queue_create("com.serial.queue", 0);
    NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"启动队列");
    for (NSUInteger index = 0; index < LMTotalNumber; index++) {
        dispatch_async(_queue, ^{
            if (!self.running) {
                return;
            }
            dispatch_source_merge_data(self->_source, 1);
            usleep(200000);//0.2秒
        });
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    [self changeStatus:self.running];
}

- (void)changeStatus:(BOOL)shouldPause {
    if (shouldPause) {
        [self pause];
    } else {
        [self resume];
    }
}

- (void)resume {
    if (self.running) {
        return;
    }
    NSLog(@"✅恢复Dispatch Source(分派源)以及_queue");
    self.running = YES;
    dispatch_resume(_source);
    if (_queue) {
        dispatch_resume(_queue);
    }
}

- (void)pause {
    if (!self.running) {
        return;
    }
    NSLog(@"🚫暂停Dispatch Source(分派源)以及_queue");
    self.running = NO;
    dispatch_suspend(_source);
    dispatch_suspend(_queue);
}

打印日志:

2020-04-17 16:30:03.200906+0800 多线程-demo[55575:1143093] ✅恢复Dispatch Source(分派源)以及_queue
2020-04-17 16:30:03.201077+0800 多线程-demo[55575:1143093] 🔴类名与方法名:-[ViewController viewDidLoad](在第71行),描述:启动队列
2020-04-17 16:30:03.217048+0800 多线程-demo[55575:1143093] 进度:0.01
2020-04-17 16:30:03.405921+0800 多线程-demo[55575:1143093] 进度:0.02
/*================省略中间====================*/
2020-04-17 16:30:06.855721+0800 多线程-demo[55575:1143093] 进度:0.19
2020-04-17 16:30:06.914987+0800 多线程-demo[55575:1143093] 🚫暂停Dispatch Source(分派源)以及_queue
2020-04-17 16:30:17.297785+0800 多线程-demo[55575:1143093] ✅恢复Dispatch Source(分派源)以及_queue
2020-04-17 16:30:17.298323+0800 多线程-demo[55575:1143093] 进度:0.2
2020-04-17 16:30:17.502854+0800 多线程-demo[55575:1143093] 进度:0.21

点击模拟器的Debug -- Simulate Memory Warning 可以实现暂停与开启。


本文首发于我的个人博客 https://limeng99.club/,转载请标明出处。

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