GCD中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 -- <NSThread: 0x600002761780>{number = 5, name = (null)}
2019-09-23 22:28:53.915212+0800 GCDSummary[53815:1472882] index = 2, str = c -- <NSThread: 0x600002765240>{number = 4, name = (null)}
2019-09-23 22:28:53.915211+0800 GCDSummary[53815:1472883] index = 1, str = b -- <NSThread: 0x60000275fb80>{number = 3, name = (null)}
2019-09-23 22:28:53.915216+0800 GCDSummary[53815:1472829] index = 0, str = a -- <NSThread: 0x600002732900>{number = 1, name = main}
2019-09-23 22:28:53.915350+0800 GCDSummary[53815:1472880] index = 4, str = e -- <NSThread: 0x600002761780>{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 -- <NSThread: 0x600001940dc0>{number = 3, name = (null)}
2019-09-24 01:05:01.062637+0800 GCDSummary[56119:1532600] index = 0, str = a -- <NSThread: 0x600001940dc0>{number = 3, name = (null)}
2019-09-24 01:05:03.065916+0800 GCDSummary[56119:1532600] index = 1, str = b -- <NSThread: 0x600001940dc0>{number = 3, name = (null)}
2019-09-24 01:05:05.068437+0800 GCDSummary[56119:1532600] index = 2, str = c -- <NSThread: 0x600001940dc0>{number = 3, name = (null)}
2019-09-24 01:05:07.069896+0800 GCDSummary[56119:1532600] index = 3, str = d -- <NSThread: 0x600001940dc0>{number = 3, name = (null)}
2019-09-24 01:05:09.075196+0800 GCDSummary[56119:1532600] index = 4, str = e -- <NSThread: 0x600001940dc0>{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 -- <NSThread: 0x600002fc5c80>{number = 3, name = (null)}
2019-09-24 01:13:30.163310+0800 GCDSummary[56241:1536487] index = 2, str = c -- <NSThread: 0x600002fcecc0>{number = 5, name = (null)}
2019-09-24 01:13:30.163373+0800 GCDSummary[56241:1536488] index = 1, str = b -- <NSThread: 0x600002fcea80>{number = 4, name = (null)}
2019-09-24 01:13:30.163345+0800 GCDSummary[56241:1536490] index = 3, str = d -- <NSThread: 0x600002ff8f40>{number = 6, name = (null)}
2019-09-24 01:13:30.163314+0800 GCDSummary[56241:1536489] index = 0, str = a -- <NSThread: 0x600002fc5c80>{number = 3, name = (null)}
2019-09-24 01:13:32.164862+0800 GCDSummary[56241:1536488] index = 4, str = e -- <NSThread: 0x600002fcea80>{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 方法使用的也是这个串行队列时,会造成死锁。

你可能感兴趣的:(iOS深入学习)