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 -- <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 方法使用的也是这个串行队列时,会造成死锁。

二,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 只使用一次

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

推荐阅读更多精彩内容