一倔叼,dispatch_apply 的用法
當(dāng)我們需要進(jìn)行循環(huán)遍歷時(shí)肛度,例如遍歷一個(gè)數(shù)組郑现,我們一般會(huì)使用 For-In 循環(huán)夕春,F(xiàn)or-In 循環(huán)會(huì)從數(shù)組第一個(gè)元素開始依次循環(huán)遍歷到最后一個(gè)元素:
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
*/
當(dāng) for 循環(huán)中的任務(wù)不多時(shí),我們可以直接使用這種方法遍歷某個(gè)容器及志,但是如果每一次循環(huán)都要執(zhí)行一個(gè)耗時(shí)操作的話該怎么辦呢片排,我們可能會(huì)立馬想到下邊的解決辦法:
// 在 for 循環(huán)中每一次任務(wù)都放到子線程中執(zhí)行
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT), ^{
[NSThread sleepForTimeInterval:1]; // 模擬耗時(shí)操作
NSLog(@"%d -- %@", i, [NSThread currentThread]);
});
}
這種辦法可取嗎寨腔?答案當(dāng)然是不可取率寡!如果每一次循環(huán)都要開啟一個(gè)子線程迫卢,那線程的無限制開啟將會(huì)耗費(fèi)非常大的資源,效率低下冶共,還可能造成應(yīng)用假死甚至程序崩潰乾蛤。
然而 GCD 給我們提供了一種快速迭代方法 dispatch_apply
,dispatch_apply
函數(shù)是dispatch_sync
函數(shù)和 Dispatch Group
的關(guān)聯(lián) API捅僵,該函數(shù)按照指定的次數(shù)將指定的任務(wù)追加到指定隊(duì)列中家卖,并等待全部任務(wù)執(zhí)行結(jié)束,系統(tǒng)會(huì)根據(jù)實(shí)際情況自動(dòng)分配和管理線程庙楚。
我們使用 dispatch_apply
方法模擬上面的for
循環(huán)任務(wù):
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]; // 模擬耗時(shí)操作
NSLog(@"%zu -- %@", index, [NSThread currentThread]);
});
});
兩種方法運(yùn)行后查看打印信息(可自行測(cè)試)上荡,可以看到,使用 dispatch_apply
方法循環(huán)打印還沒有直接在 for 循環(huán)中不斷開啟線程速度快馒闷。但是我們應(yīng)當(dāng)注意酪捡,for 循環(huán)中不斷開啟線程造成了大量線程的開啟(模擬器測(cè)試開啟了70個(gè)左右的子線程),根本無法顧忌系統(tǒng)性能纳账,而dispatch_apply
方法則可以智能管理線程沛善,始終是五六個(gè)子線程循環(huán)使用,這也是導(dǎo)致速度較慢的原因塞祈。實(shí)際開發(fā)中如果不考慮系統(tǒng)性能肆意開啟子線程,可能會(huì)出現(xiàn)線程擁堵(假死)帅涂、程序崩潰的情況议薪。
下面,我們對(duì)上面例子中的數(shù)組使用 dispatch_apply 方法進(jìn)行迭代:
NSLog(@"-- begin --");
NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];
dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT);
/*
arr.count 指定重復(fù)次數(shù) 這里數(shù)組內(nèi)元素個(gè)數(shù)是 5 個(gè)媳友,也就是重復(fù) 5 次
queue 追加任務(wù)的隊(duì)列
index 帶有參數(shù)的 Block, index 的作用是為了按執(zhí)行的順序區(qū)分各個(gè) Block
*/
dispatch_apply(arr.count, queue, ^(size_t index) {
NSLog(@"index = %zu, str = %@ -- %@", index, arr[index], [NSThread currentThread]);
});
NSLog(@"-- end --");
運(yùn)行后查看打印信息:
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
方法中的任務(wù)是并發(fā)執(zhí)行的,但是阻塞了當(dāng)前線程(主線程)醇锚,當(dāng)所有任務(wù)執(zhí)行完畢后才打印了“-- end --”哼御。所以在我們使用dispatch_apply
方法時(shí),應(yīng)當(dāng)在外邊套上一層 dispatch_async
焊唬。
另外恋昼,當(dāng) index 等于 0 的時(shí)候(第一個(gè)任務(wù)執(zhí)行時(shí)),任務(wù)一定是在當(dāng)前線程(該例中的當(dāng)前線程是主線程)中執(zhí)行赶促,這一點(diǎn)可以理解為除第一個(gè)任務(wù)的其他任務(wù)都是依照和第一個(gè)任務(wù)并發(fā)的目的放進(jìn)了并發(fā)隊(duì)列里液肌。
注意:dispatch_apply
方法非常適合大量字典數(shù)據(jù)轉(zhuǎn)模型。
有一個(gè)現(xiàn)象需要注意鸥滨,先看代碼:
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 --");
運(yùn)行后查看打印信息:
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)}
可以看到嗦哆,外部的異步執(zhí)行所在的隊(duì)列和 dispatch_apply
所在的隊(duì)列是同一個(gè)并發(fā)隊(duì)列谤祖。由于整個(gè) dispatch_apply
方法被放在了異步執(zhí)行中,所以所有任務(wù)都在子線程中執(zhí)行老速。
需要注意的是:子線程卻只開啟了一個(gè)粥喜,在 dispatch_apply
方法中各個(gè)任務(wù)所在的子線程和外部異步執(zhí)行所在的子線程是同一個(gè),這也就導(dǎo)致了 5 次打印依次間隔 2 秒執(zhí)行(無法開啟更多線程支持并發(fā)執(zhí)行任務(wù))橘券。
經(jīng)過多次試驗(yàn)额湘,發(fā)現(xiàn)問題出在這個(gè)并發(fā)隊(duì)列 dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT)
; 身上,當(dāng)我們使用 dispatch_queue_create
方法創(chuàng)建并發(fā)隊(duì)列并在內(nèi)外兩層都使用這個(gè)隊(duì)列時(shí)约郁,任務(wù)的執(zhí)行就變成了串行執(zhí)行(只開啟了一個(gè)子線程并且只能用這個(gè)子線程執(zhí)行任務(wù))缩挑。
這里還有一個(gè)情況:如果我們內(nèi)外兩層都使用 dispatch_get_global_queue
方法來獲取并行隊(duì)列,即上述代碼第三行修改為 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
的話,所有任務(wù)可以正常并發(fā)執(zhí)行绽快,這是為什么呢芥丧?
首先看一下兩種并發(fā)隊(duì)列的區(qū)別:
Global queues
是全局并發(fā)隊(duì)列,由整個(gè)進(jìn)程共享坊罢。進(jìn)程中存在三個(gè)全局隊(duì)列:高续担、中(默認(rèn))、低三個(gè)優(yōu)先級(jí)隊(duì)列活孩∥镉觯可以調(diào)用 dispatch_get_global_queue
函數(shù)傳入優(yōu)先級(jí)來訪問隊(duì)列。
dispatch_queue_create
創(chuàng)建出來的是用戶隊(duì)列憾儒,由用戶通過 dispatch_queue_create
函數(shù)自行創(chuàng)建询兴。
我們發(fā)現(xiàn)全局并發(fā)隊(duì)列是一直存在的,我們調(diào)用 dispatch_get_global_queue
函數(shù)只是獲得了對(duì)其的訪問使用權(quán)起趾,指定優(yōu)先級(jí)可以向系統(tǒng)說明我們要在哪個(gè)級(jí)別的全局并發(fā)隊(duì)列中處理任務(wù)诗舰,而 dispatch_queue_create
函數(shù)是創(chuàng)建了自定義隊(duì)列。
造成上述情況的原因有可能是因?yàn)槿植l(fā)隊(duì)列處理任務(wù)的底層邏輯和自定義并發(fā)隊(duì)列不同训裆,因?yàn)檎麄€(gè)進(jìn)程可以共享全局并發(fā)隊(duì)列眶根,所以其開啟子線程或者利用(分配)已開啟的線程的能力(權(quán)限)更強(qiáng)大,可以為內(nèi)外兩層函數(shù)開啟不同的子線程边琉。
特別注意:如下兩種情況會(huì)造成死鎖:
// 1属百、在主線程中使用主線程執(zhí)行 dispatch_apply 方法
dispatch_apply(10, dispatch_get_main_queue(), ^(size_t index) {
NSLog(@"%zu", index);
});
// 2、在串行隊(duì)列中使用該串行隊(duì)列執(zhí)行 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);
});
});
前邊有提到变姨,主線程隊(duì)列是一種特殊的串行隊(duì)列诸老,所以說是兩種情況,其實(shí)可以歸結(jié)為一種:當(dāng)在串行隊(duì)列中執(zhí)行 dispatch_apply
方法,且 dispatch_apply
方法使用的也是這個(gè)串行隊(duì)列時(shí)别伏,會(huì)造成死鎖蹄衷。
二,dispatch_after 的用法
dispatch_after
是來延遲執(zhí)行的GCD
方法厘肮,因?yàn)樵谥骶€程中我們不能用sleep
來延遲方法的調(diào)用愧口,所以用dispatch_after
是最合適的
dispatch_after
能讓我們添加進(jìn)隊(duì)列的任務(wù)延時(shí)執(zhí)行,該函數(shù)并不是在指定時(shí)間后執(zhí)行處理类茂,而只是在指定時(shí)間追加處理到dispatch_queue
代碼實(shí)現(xiàn)
//該方法的第一個(gè)參數(shù)是time耍属,第二個(gè)參數(shù)是dispatch_queue,第三個(gè)參數(shù)是要執(zhí)行的block巩检。
//在主線程中延遲執(zhí)行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
特別注意
上面這句dispatch_after的真正含義是在6秒后把任務(wù)添加進(jìn)隊(duì)列中厚骗,并不是表示在6秒后執(zhí)行,大部分情況該函數(shù)能達(dá)到我們的預(yù)期兢哭,只有在對(duì)時(shí)間要求非常精準(zhǔn)的情況下才可能會(huì)出現(xiàn)問題领舰。