多線程有幾個(gè)重要的概念青伤,任務(wù)、隊(duì)列殴瘦、線程狠角。
任務(wù):是指執(zhí)行什么樣的操作,在GCD中就是block蚪腋。
隊(duì)列:用來(lái)存放任務(wù)丰歌,分為串行隊(duì)列和并行隊(duì)列。放在串行隊(duì)列中的任務(wù)要等到正在執(zhí)行的任務(wù)執(zhí)行完后才會(huì)被執(zhí)行屉凯;放在并行隊(duì)列中的任務(wù)不用等到正在執(zhí)行的任務(wù)執(zhí)行完就可以被執(zhí)行立帖。
線程:執(zhí)行任務(wù)需要線程,線程從隊(duì)列中以先進(jìn)先出(FIFO)的方式取出任務(wù)執(zhí)行神得,線程一次只能執(zhí)行一個(gè)任務(wù)厘惦。
這三者的關(guān)系就是:線程從隊(duì)列中取任務(wù)執(zhí)行。
GCD中的隊(duì)列:
dispatch_main_queue
主隊(duì)列哩簿,程序啟動(dòng)時(shí)與主線程一起由系統(tǒng)自動(dòng)創(chuàng)建宵蕉,UI操作都放在主隊(duì)列中
dispatch_get_global_queu
全局并發(fā)隊(duì)列,由系統(tǒng)定義节榜,調(diào)用函數(shù)dispatch_get_global_queue(identifier: Int, flags: Uint)獲取羡玛。identifier以前叫做優(yōu)先級(jí),現(xiàn)在稱為服務(wù)質(zhì)量宗苍,現(xiàn)在傳入優(yōu)先級(jí)的宏定義也可以稼稿,目測(cè)優(yōu)先級(jí)的界限并不明顯,所以平時(shí)直接傳入0讳窟,見(jiàn)下面對(duì)應(yīng)關(guān)系让歼。flags是系統(tǒng)保留的一個(gè)參數(shù),傳入0即可丽啡。
DISPATCH_QUEUE_PRIORITY_HIGH QOS_CLASS_USER_INITIATED 2
DISPATCH_QUEUE_PRIORITY_DEFAULT QOS_CLASS_DEFAULT 0
DISPATCH_QUEUE_PRIORITY_LOW QOS_CLASS_UTILITY -2
DISPATCH_QUEUE_PRIORITY_BACKGROUND QOS_CLASS_BACKGROUND INT16_MIN
對(duì)于同一優(yōu)先級(jí)谋右,獲取到的全局并發(fā)隊(duì)列是同一個(gè); 不同優(yōu)先級(jí),獲取到的全局并發(fā)隊(duì)列也不同补箍「闹矗看下面獲取全局并發(fā)隊(duì)列
NSLog(@"%@",dispatch_get_global_queue(0, 0));
NSLog(@"%@",dispatch_get_global_queue(0, 0));
NSLog(@"%@",dispatch_get_global_queue(2, 0));
NSLog(@"%@",dispatch_get_global_queue(2, 0));
dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)
開(kāi)發(fā)者自己創(chuàng)建的隊(duì)列,參數(shù)label是該隊(duì)列的名字坑雅,可以通過(guò)
dispatch_queue_get_label(<#dispatch_queue_t _Nullable queue#>)
獲取辈挂。參數(shù)attr傳入DISPATCH_QUEUE_SERIAL或nil返回串行隊(duì)列,傳入DISPATCH_QUEUE_CONCURRENT返回并行隊(duì)列裹粤。
dispatch_queue_create
創(chuàng)建的優(yōu)先級(jí)與全局并發(fā)隊(duì)列的默認(rèn)優(yōu)先級(jí)是同一級(jí)別终蒂,也就是說(shuō)創(chuàng)建的隊(duì)列的優(yōu)先級(jí)為默認(rèn)優(yōu)先級(jí)。至于怎么改變它的優(yōu)先級(jí)需要調(diào)用dispatch_set_target_queue
,這個(gè)函數(shù)后面描述后豫。
調(diào)度函數(shù)
dispatch_sync
將任務(wù)提交到相應(yīng)隊(duì)列悉尾,同步執(zhí)行,不開(kāi)新線程
dispatch_async
將任務(wù)提交到相應(yīng)隊(duì)列挫酿,異步執(zhí)行构眯,如果從主隊(duì)列取任務(wù),不開(kāi)新線程早龟,在主線程中執(zhí)行惫霸;其他隊(duì)列,開(kāi)新線程執(zhí)行
現(xiàn)在來(lái)看看調(diào)度函數(shù)與各種隊(duì)列的組合后的情況:
1葱弟、dispatch_sync 與 串行隊(duì)列:
(1)在串行隊(duì)列queue中調(diào)用該函數(shù)壹店、且該函數(shù)是從串行隊(duì)列queue中去任務(wù)執(zhí)行,就會(huì)出現(xiàn)線程死鎖芝加。 例如下面兩種情況:
在主線程中調(diào)用
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"在主線程中調(diào)用dispatch_sync硅卢,并且該函數(shù)第一個(gè)參數(shù)傳入的是主隊(duì)列,則會(huì)出現(xiàn)線程死鎖");
});
在同步執(zhí)行任務(wù)的子線程中調(diào)用
dispatch_queue_t queue = dispatch_queue_create("串行隊(duì)列", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
/*
其他代碼
*/
dispatch_sync(queue, ^{
NSLog(@"queue是串行隊(duì)列就會(huì)出現(xiàn)線程死鎖");
});
});
(2)該串行隊(duì)列與當(dāng)前隊(duì)列不同藏杖,不會(huì)出現(xiàn)線程死鎖将塑。在當(dāng)前線程中同步執(zhí)行。dispatch_sync執(zhí)行完后它后面的代碼才會(huì)執(zhí)行蝌麸。
2点寥、dispatch_sync 與 并行隊(duì)列:
因?yàn)閐ispatch_sync不開(kāi)線程,并且線程一次只能執(zhí)行一個(gè)任務(wù)来吩,所以依然是敢辩,dispatch_sync執(zhí)行完后它后面的代碼才會(huì)執(zhí)行。
3弟疆、dispatch_async 與 串行隊(duì)列:
(1)主隊(duì)列戚长,不開(kāi)新線程,任務(wù)將在主線程中執(zhí)行怠苔。但不會(huì)馬上執(zhí)行同廉,而是將任務(wù)從棧copy到堆,等待主隊(duì)列中棧區(qū)的任務(wù)執(zhí)行完嘀略,再執(zhí)行堆區(qū)的任務(wù)恤溶。因此
dispatch_async(dispatch_get_main_queue(), ^{
/*
在主線程執(zhí)行任務(wù)
*/
});
有兩個(gè)作用:1)子線程處理耗時(shí)操作后回到主線程處理UI; 2)延時(shí)乓诽。
(2)開(kāi)發(fā)者創(chuàng)建的串行隊(duì)列帜羊,開(kāi)新線程同步執(zhí)行。
4鸠天、dispatch_async 與 并發(fā)隊(duì)列:
開(kāi)新線程并發(fā)執(zhí)行任務(wù)讼育,至于開(kāi)多少條線程由系統(tǒng)決定。看下面的代碼以及運(yùn)行結(jié)果:
for (int i = 0; i < 20; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"線程%@執(zhí)行第%d個(gè)任務(wù)",[NSThread currentThread],i);
});
}
dispatch_after
延時(shí)操作,調(diào)用函數(shù)
dispatch_after(<#dispatch_time_t when#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
參數(shù)when 指定時(shí)間饥瓷,傳入DISPATCH_TIME_NOW,效果等同于dispatch_async痹籍;傳入DISPATCH_TIME_FOREVER呢铆,任務(wù)將永不執(zhí)行;所以一般傳入dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC))
t*NSEC_PER_SEC代表t秒蹲缠。
參數(shù)queue可以是任意隊(duì)列棺克,一般傳入主隊(duì)列。
dispatch_group
調(diào)度組
幾個(gè)任務(wù)并發(fā)執(zhí)行线定,并且等到都執(zhí)行完后再做其他操作娜谊,就可調(diào)使用該函數(shù)。例如異步下載小說(shuō)A斤讥、B纱皆、C,下載完后提示用戶芭商,代碼如下:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"下載小說(shuō)A");
});
dispatch_group_async(group, queue, ^{
NSLog(@"下載小說(shuō)B");
});
dispatch_group_async(group, queue, ^{
NSLog(@"下載小說(shuō)C");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有小說(shuō)下載完成");
});
也可將
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有小說(shuō)下載完成");
});
替換成
BOOL flag = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if (flag) {
NSLog(@"所有小說(shuō)下載完成");
} else {
NSLog(@"小說(shuō)下載超時(shí)");
}
dispatch_group_wait的第二個(gè)參數(shù)可傳入指定時(shí)間派草。
dispatch_once
一次性操作
調(diào)用函數(shù)
dispatch_once(<#dispatch_once_t * _Nonnull predicate#>, <#^(void)block#>)
參數(shù)predicate 是用來(lái)標(biāo)識(shí)block是否執(zhí)行的指針,必須是全局或靜態(tài)變量蓉坎,并且該指針?biāo)赶虻膮^(qū)域的初始值為0澳眷,當(dāng)任務(wù)執(zhí)行完畢,系統(tǒng)會(huì)將其值置為-1蛉艾。
這個(gè)函數(shù)用于初始化全局?jǐn)?shù)據(jù)钳踊,并且保證線程安全,常用于OC中的單例模式勿侯。
dispatch_apply
dispatch_sync 與 dispatch_group 的結(jié)合
調(diào)用函數(shù)
dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t _Nonnull queue#>, <#^(size_t)block#>)
參數(shù) iterations 將任務(wù)循環(huán)添加到指定隊(duì)列的次數(shù)拓瞪; block的參數(shù)是循環(huán)下標(biāo)(從0開(kāi)始)
調(diào)用該函數(shù)會(huì)將queue中的任務(wù)執(zhí)行完才執(zhí)行它后面的代碼。queue如果是串行隊(duì)列助琐,queue中的任務(wù)會(huì)在當(dāng)前線程中同步執(zhí)行荞膘;queue如果是并行隊(duì)列,當(dāng)前線程會(huì)取出queue中的第一個(gè)任務(wù)執(zhí)行卡辰,然后開(kāi)新線程執(zhí)行后面的任務(wù)钠右,所開(kāi)線程個(gè)數(shù)由系統(tǒng)決定。因?yàn)閐ispatch_apply具有dispatch_sync的特征掘譬,所以如果queue是串行隊(duì)列并且與當(dāng)前的隊(duì)列是同一個(gè)對(duì)象時(shí)泰演,就會(huì)出現(xiàn)線程死鎖。
dispatch_apply主要用于異步并發(fā)處理數(shù)據(jù)葱轩,并且處理完后統(tǒng)一操作睦焕。所以dispatch_apply一般在子線程中調(diào)用藐握。當(dāng)然,像這樣的操作用dispatch_group也可以實(shí)現(xiàn)垃喊。但是猾普,思考這種情況,如果對(duì)象數(shù)組本谜,里面所有元素需要并發(fā)執(zhí)行某種操作初家,并且都執(zhí)行完之后要統(tǒng)一做處理,這時(shí)如果用dispatch_group乌助,則需要for in 遍歷數(shù)組笤成,用dispatch_apply則可以省去這種遍歷。代碼如下:
dispatch_async(queue, ^{
dispatch_apply(arr.count, dispatch_get_global_queue(0, 0), ^(size_t i) {
NSLog(@"%@",arr[i]);
});
});
dispatch_barrier_async
在處理數(shù)據(jù)讀取時(shí)眷茁,為了避免數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題炕泳,寫(xiě)入操作與寫(xiě)入操作或讀取操作不能并發(fā)執(zhí)行。針對(duì)這個(gè)例子上祈,可以使用dispatch_barrier_async將寫(xiě)入操作提交到并發(fā)隊(duì)列中培遵,此時(shí)被提交的任務(wù)暫不執(zhí)行,當(dāng)他前面的任務(wù)執(zhí)行完畢時(shí)在執(zhí)行該任務(wù)登刺,等到該任務(wù)執(zhí)行完畢籽腕,后面提交的才執(zhí)行。需要注意的是指定的queue應(yīng)該是通過(guò)dispatch_queue_create創(chuàng)建的纸俭,系統(tǒng)定義的全局并發(fā)隊(duì)列無(wú)效皇耗。示例代碼如下:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"讀取操作1");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作2");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作3");
});
dispatch_barrier_async(queue, ^{
NSLog(@"寫(xiě)入操作1");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作4");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作5");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作6");
});
dispatch_barrier_async(queue, ^{
NSLog(@"寫(xiě)入操作2");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作7");
});
dispatch_async(queue, ^{
NSLog(@"讀取操作8");
});
與之對(duì)應(yīng)的是
dispatch_barrier_sync
,該函數(shù)會(huì)阻塞當(dāng)前線程揍很。它也會(huì)等待前面提交的任務(wù)執(zhí)行完畢在執(zhí)行該函數(shù)提交的任務(wù)郎楼,等它執(zhí)行完畢后面的任務(wù)才提交到queue中執(zhí)行
dispatch_semaphore
信號(hào)量。 簡(jiǎn)單來(lái)說(shuō)就是控制訪問(wèn)資源的數(shù)量窒悔,比如系統(tǒng)有兩個(gè)資源可以被利用呜袁,同時(shí)有三個(gè)線程要訪問(wèn),只能允許兩個(gè)線程訪問(wèn)简珠,第三個(gè)應(yīng)當(dāng)?shù)却Y源被釋放后再訪問(wèn)阶界。
下面逐一介紹與之相關(guān)的三個(gè)函數(shù):
(1)dispatch_semaphore_create(<#long value#>)
創(chuàng)建信號(hào)量,返回值類型dispatch_semaphore_t
參數(shù)value 為允許訪問(wèn)資源的線程數(shù)聋庵,該值必須 >= 0膘融,否則會(huì)返回nil。當(dāng)value為0時(shí)對(duì)訪問(wèn)資源的線程沒(méi)有限制祭玉。
(2)dispatch_semaphore_signal(<#dispatch_semaphore_t _Nonnull dsema#>)
信號(hào)量+1
(3)dispatch_semaphore_wait(<#dispatch_semaphore_t _Nonnull dsema#>, <#dispatch_time_t timeout#>
該函數(shù)會(huì)讓信號(hào)量-1
這個(gè)函數(shù)的具體作用是這樣的氧映,如果dsema信號(hào)量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語(yǔ)句攘宙,并且將信號(hào)量的值減1屯耸,返回值為0;如果desema的值為0蹭劈,那么這個(gè)函數(shù)就阻塞當(dāng)前線程等待timeout疗绣,如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了,且該函數(shù)所處線程獲得了信號(hào)量铺韧,那么就繼續(xù)向下執(zhí)行并將信號(hào)量減1, 返回值為0多矮。如果等待期間沒(méi)有獲取到信號(hào)量或者信號(hào)量的值一直為0,那么等到timeout時(shí)哈打,其所處線程自動(dòng)執(zhí)行其后語(yǔ)句塔逃,返回值大于0。通過(guò)返回值是否為0料仗,可以判斷等待是否超時(shí)湾盗。
第二個(gè)函數(shù)和第一個(gè)函數(shù)是配套使用的,使用時(shí)先調(diào)用函數(shù)(3)將信號(hào)量-1立轧,后調(diào)用函數(shù)(2)將信號(hào)量+1
為了加深理解格粪,舉一個(gè)在餐廳排隊(duì)吃放的例子。假設(shè)某餐廳有50個(gè)座位氛改,這相當(dāng)于調(diào)用dispatch_semaphore_wait(value: Int)參數(shù)傳入50帐萎,有顧客來(lái)吃飯相當(dāng)于函數(shù)dispatch_semaphore_wait(dsema: dispatch_semaphore_t, timeout: dispatch_time_t),前50個(gè)顧客都有座位直接就餐胜卤。第51個(gè)顧客來(lái)到時(shí)就需要等待前面的顧客吃完飯離開(kāi)疆导,他才能就坐用餐。此時(shí)相當(dāng)于dispatch_semaphore_wait函數(shù)阻塞當(dāng)前線程葛躏。當(dāng)有顧客用餐完畢離開(kāi)時(shí)澈段,此時(shí)就有了一個(gè)空位。相當(dāng)于dispatch_semaphore_signal將型號(hào)來(lái)加1舰攒,第51個(gè)顧客等到座位均蜜,相當(dāng)于dispatch_semaphore_wait返回值為0。
信號(hào)量是GCD同步的一種方式芒率。前面介紹過(guò)dispatch_barrier_async是對(duì)queue中的任務(wù)進(jìn)行批量同步處理囤耳,dispatch_sync是對(duì)queue中的任務(wù)單個(gè)同步處理,而dispatch_semaphore是對(duì)queue中的某個(gè)任務(wù)中的某部分(某段代碼)同步處理偶芍。此時(shí)將dispatch_semaphore_wait中的參數(shù)傳入1充择。
dispatch_semaphore 的使用如下:
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
/*
其他并發(fā)操作
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
同步操作,例如 [arr addObject:@(i)];
*/
[arr addObject:@(i)];
dispatch_semaphore_signal(semaphore);
/*
其他并發(fā)操作
*/
});
}
dispatch_suspend / dispatch_resume
暫停指定的隊(duì)列 / 恢復(fù)指定的隊(duì)列
有時(shí)候獲取希望提交到queue中的任務(wù)暫不執(zhí)行匪蟀,等待某一時(shí)刻執(zhí)行椎麦,這時(shí)候就可使用dispatch_suspend 和 dispatch_resume, 使用dispatch_suspend時(shí)對(duì)正在執(zhí)行的任務(wù)不會(huì)起作用。
dispatch_suspend使指定隊(duì)列的暫停計(jì)數(shù)+1观挎,dispatch_resume使指定隊(duì)列的暫停計(jì)數(shù)-1。
使用它們時(shí)要注意幾點(diǎn):
(1)dispatch_suspend 與 dispatch_resume 成對(duì)出現(xiàn) 造成,否則程序在運(yùn)行時(shí)會(huì)crash
(2)dispatch_suspend 與 dispatch_resume 中指定的隊(duì)列是通過(guò) dispatch_queue_create 創(chuàng)建的晒屎,其他隊(duì)列無(wú)效
dispatch_set_target_queue
前面提到過(guò),我們自己創(chuàng)建的隊(duì)列的優(yōu)先級(jí)是默認(rèn)優(yōu)先級(jí)骇吭,需要更改優(yōu)先級(jí)需要調(diào)用dispatch_set_target_queue(系統(tǒng)的隊(duì)列優(yōu)先級(jí)不能修改)绵跷。實(shí)際上碾局,dispatch_set_target_queue 主要有兩個(gè)作用:
(1)更改 dispatch_queue_create 函數(shù)創(chuàng)建的隊(duì)列的優(yōu)先級(jí)净当。代碼如下:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(queue, dispatch_get_global_queue(0, 0));
函數(shù)中第一個(gè)是需要變更優(yōu)先級(jí)的隊(duì)列像啼,第二個(gè)參數(shù)是需要傳入全局并發(fā)隊(duì)列忽冻,并且優(yōu)先級(jí)是第一個(gè)參數(shù)想要的優(yōu)先級(jí)。
(2)修改用戶隊(duì)列的目標(biāo)隊(duì)列湖笨,使多個(gè)serial queue在目標(biāo)queue上一次只有一個(gè)執(zhí)行慈省。第二個(gè)參數(shù)是目標(biāo)隊(duì)列边败,第一個(gè)參數(shù)是任務(wù)將放在目標(biāo)隊(duì)列中的另一串行隊(duì)列致燥。
例如,用戶要依次下載小說(shuō)A匾寝、B艳悔、C,但下載任務(wù)放在不同的串行隊(duì)列中乔外,這時(shí)就可以依次調(diào)用dispatch_set_target_queue杨幼,將放有下載任務(wù)的隊(duì)列作為第一個(gè)參數(shù)傳入差购,讓任務(wù)將目標(biāo)隊(duì)列中同步執(zhí)行欲逃。 示例代碼如下:
dispatch_queue_t targetQueue = dispatch_queue_create("targetQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"開(kāi)始下載小說(shuō)A");
/*
下載中
*/
NSLog(@"小說(shuō)A下載完成");
});
dispatch_async(queue2, ^{
NSLog(@"開(kāi)始下載小說(shuō)B");
/*
下載中
*/
NSLog(@"小說(shuō)B下載完成");
});
dispatch_async(queue3, ^{
NSLog(@"開(kāi)始下載小說(shuō)C");
/*
下載中
*/
NSLog(@"小說(shuō)C下載完成");
});