483,dispatch_apply和dispatch_once和dispatch_after的区别(面试点:外部的异步执行所在的队列和 dispatch_apply 所在的队列是同一个并发队列...

一,dispatch_apply 的用法

当我们需要进行循环遍历时,例如遍历一个数组,我们一般会使用 For-In 循环,For-In 循环会从数组第一个元素开始依次循环遍历到最后一个元素:

NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];
    
for (NSString *str in arr) {
    NSLog(@"str = %@", str);
}

// 打印:
/*
2019-09-23 21:23:21.387010+0800 GCDSummary[52959:1452344] str = a
2019-09-23 21:23:21.387119+0800 GCDSummary[52959:1452344] str = b
2019-09-23 21:23:21.387205+0800 GCDSummary[52959:1452344] str = c
2019-09-23 21:23:21.387287+0800 GCDSummary[52959:1452344] str = d
2019-09-23 21:23:21.387366+0800 GCDSummary[52959:1452344] str = e
*/

当 for 循环中的任务不多时,我们可以直接使用这种方法遍历某个容器,但是如果每一次循环都要执行一个耗时操作的话该怎么办呢,我们可能会立马想到下边的解决办法:

// 在 for 循环中每一次任务都放到子线程中执行
for (int i = 0; i < 1000; i++) {
    dispatch_async(dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT), ^{
        [NSThread sleepForTimeInterval:1]; // 模拟耗时操作
        NSLog(@"%d -- %@", i, [NSThread currentThread]);
    });
}

这种办法可取吗?答案当然是不可取!如果每一次循环都要开启一个子线程,那线程的无限制开启将会耗费非常大的资源,效率低下,还可能造成应用假死甚至程序崩溃。

然而 GCD 给我们提供了一种快速迭代方法 dispatch_applydispatch_apply函数是dispatch_sync 函数和 Dispatch Group 的关联 API,该函数按照指定的次数将指定的任务追加到指定队列中,并等待全部任务执行结束,系统会根据实际情况自动分配和管理线程。

我们使用 dispatch_apply方法模拟上面的for循环任务:

dispatch_async(dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT), ^{
    dispatch_apply(1000, dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT), ^(size_t index) {
        [NSThread sleepForTimeInterval:1]; // 模拟耗时操作
        NSLog(@"%zu -- %@", index, [NSThread currentThread]);
    });
});

两种方法运行后查看打印信息(可自行测试),可以看到,使用 dispatch_apply 方法循环打印还没有直接在 for 循环中不断开启线程速度快。但是我们应当注意,for 循环中不断开启线程造成了大量线程的开启(模拟器测试开启了70个左右的子线程),根本无法顾忌系统性能,而dispatch_apply方法则可以智能管理线程,始终是五六个子线程循环使用,这也是导致速度较慢的原因。实际开发中如果不考虑系统性能肆意开启子线程,可能会出现线程拥堵(假死)、程序崩溃的情况。

下面,我们对上面例子中的数组使用 dispatch_apply 方法进行迭代:

NSLog(@"-- begin --");
    
NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];
    
dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT);

/*
arr.count    指定重复次数  这里数组内元素个数是 5 个,也就是重复 5 次
queue        追加任务的队列
index        带有参数的 Block, index 的作用是为了按执行的顺序区分各个 Block
*/
dispatch_apply(arr.count, queue, ^(size_t index) {
    NSLog(@"index = %zu, str = %@ -- %@", index, arr[index], [NSThread currentThread]);
});

NSLog(@"-- end --");

运行后查看打印信息:

2019-09-23 22:28:53.914873+0800 GCDSummary[53815:1472829] -- begin --
2019-09-23 22:28:53.915211+0800 GCDSummary[53815:1472880] index = 3, str = d -- {number = 5, name = (null)}
2019-09-23 22:28:53.915212+0800 GCDSummary[53815:1472882] index = 2, str = c -- {number = 4, name = (null)}
2019-09-23 22:28:53.915211+0800 GCDSummary[53815:1472883] index = 1, str = b -- {number = 3, name = (null)}
2019-09-23 22:28:53.915216+0800 GCDSummary[53815:1472829] index = 0, str = a -- {number = 1, name = main}
2019-09-23 22:28:53.915350+0800 GCDSummary[53815:1472880] index = 4, str = e -- {number = 5, name = (null)}
2019-09-23 22:28:53.915476+0800 GCDSummary[53815:1472829] -- end --

可以看到,dispatch_apply 方法中的任务是并发执行的,但是阻塞了当前线程(主线程),当所有任务执行完毕后才打印了“-- end --”。所以在我们使用dispatch_apply 方法时,应当在外边套上一层 dispatch_async

另外,当 index 等于 0 的时候(第一个任务执行时),任务一定是在当前线程(该例中的当前线程是主线程)中执行,这一点可以理解为除第一个任务的其他任务都是依照和第一个任务并发的目的放进了并发队列里。

注意dispatch_apply 方法非常适合大量字典数据转模型

有一个现象需要注意,先看代码:

NSLog(@"-- begin --");

NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];

dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    dispatch_apply(arr.count, queue, ^(size_t index) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"index = %zu, str = %@ -- %@", index, arr[index], [NSThread currentThread]);
    });

});

NSLog(@"-- end --");

运行后查看打印信息:

2019-09-24 01:04:59.061687+0800 GCDSummary[56119:1532552] -- begin --
2019-09-24 01:04:59.061909+0800 GCDSummary[56119:1532552] -- end --
2019-09-24 01:04:59.062015+0800 GCDSummary[56119:1532600] current thread -- {number = 3, name = (null)}
2019-09-24 01:05:01.062637+0800 GCDSummary[56119:1532600] index = 0, str = a -- {number = 3, name = (null)}
2019-09-24 01:05:03.065916+0800 GCDSummary[56119:1532600] index = 1, str = b -- {number = 3, name = (null)}
2019-09-24 01:05:05.068437+0800 GCDSummary[56119:1532600] index = 2, str = c -- {number = 3, name = (null)}
2019-09-24 01:05:07.069896+0800 GCDSummary[56119:1532600] index = 3, str = d -- {number = 3, name = (null)}
2019-09-24 01:05:09.075196+0800 GCDSummary[56119:1532600] index = 4, str = e -- {number = 3, name = (null)}

可以看到,外部的异步执行所在的队列和 dispatch_apply 所在的队列是同一个并发队列。由于整个 dispatch_apply 方法被放在了异步执行中,所以所有任务都在子线程中执行。

需要注意的是:子线程却只开启了一个,在 dispatch_apply方法中各个任务所在的子线程和外部异步执行所在的子线程是同一个,这也就导致了 5 次打印依次间隔 2 秒执行(无法开启更多线程支持并发执行任务)。

经过多次试验,发现问题出在这个并发队列 dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT); 身上,当我们使用 dispatch_queue_create 方法创建并发队列并在内外两层都使用这个队列时,任务的执行就变成了串行执行(只开启了一个子线程并且只能用这个子线程执行任务)。

这里还有一个情况:如果我们内外两层都使用 dispatch_get_global_queue 方法来获取并行队列,即上述代码第三行修改为 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); 的话,打印信息如下:

2019-09-24 01:13:28.157639+0800 GCDSummary[56241:1536440] -- begin --
2019-09-24 01:13:28.157839+0800 GCDSummary[56241:1536440] -- end --
2019-09-24 01:13:28.157902+0800 GCDSummary[56241:1536489] current thread -- {number = 3, name = (null)}
2019-09-24 01:13:30.163310+0800 GCDSummary[56241:1536487] index = 2, str = c -- {number = 5, name = (null)}
2019-09-24 01:13:30.163373+0800 GCDSummary[56241:1536488] index = 1, str = b -- {number = 4, name = (null)}
2019-09-24 01:13:30.163345+0800 GCDSummary[56241:1536490] index = 3, str = d -- {number = 6, name = (null)}
2019-09-24 01:13:30.163314+0800 GCDSummary[56241:1536489] index = 0, str = a -- {number = 3, name = (null)}
2019-09-24 01:13:32.164862+0800 GCDSummary[56241:1536488] index = 4, str = e -- {number = 4, name = (null)}

可以看到,使用 dispatch_get_global_queue 的话,所有任务可以正常并发执行,这是为什么呢?

首先看一下两种并发队列的区别:

Global queues 是全局并发队列,由整个进程共享。进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用 dispatch_get_global_queue 函数传入优先级来访问队列。

dispatch_queue_create 创建出来的是用户队列,由用户通过 dispatch_queue_create 函数自行创建。

我们发现全局并发队列是一直存在的,我们调用 dispatch_get_global_queue 函数只是获得了对其的访问使用权,指定优先级可以向系统说明我们要在哪个级别的全局并发队列中处理任务,而 dispatch_queue_create 函数是创建了自定义队列。

造成上述情况的原因有可能是因为全局并发队列处理任务的底层逻辑和自定义并发队列不同,因为整个进程可以共享全局并发队列,所以其开启子线程或者利用(分配)已开启的线程的能力(权限)更强大,可以为内外两层函数开启不同的子线程。

特别注意:如下两种情况会造成死锁:
// 1、在主线程中使用主线程执行 dispatch_apply 方法
dispatch_apply(10, dispatch_get_main_queue(), ^(size_t index) {
    NSLog(@"%zu", index);
});
// 2、在串行队列中使用该串行队列执行 dispatch_apply 方法
dispatch_queue_t queue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zu", index);
    });
});

前边有提到,主线程队列是一种特殊的串行队列,所以说是两种情况,其实可以归结为一种:当在串行队列中执行 dispatch_apply 方法,且 dispatch_apply 方法使用的也是这个串行队列时,会造成死锁。

二,dispatch_after 的用法

dispatch_after是来延迟执行的GCD方法,因为在主线程中我们不能用sleep来延迟方法的调用,所以用dispatch_after是最合适的

dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue

代码实现

//该方法的第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block。
    //在主线程中延迟执行
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
    });

特别注意

上面这句dispatch_after的真正含义是在6秒后把任务添加进队列中,并不是表示在6秒后执行,大部分情况该函数能达到我们的预期,只有在对时间要求非常精准的情况下才可能会出现问题。

三,dispatch_once 只使用一次

你可能感兴趣的:(483,dispatch_apply和dispatch_once和dispatch_after的区别(面试点:外部的异步执行所在的队列和 dispatch_apply 所在的队列是同一个并发队列...)