1 GCD 術(shù)語
1.1 Serial vs. Concurrent 串行 vs. 并發(fā)
概念:該術(shù)語描述執(zhí)行當(dāng)前任務(wù)與其他任務(wù)之間的關(guān)系签舞。串行執(zhí)行意味著每次只有一個(gè)任務(wù)被執(zhí)行戒良;并發(fā)執(zhí)行即同一時(shí)間可以有多個(gè)任務(wù)被執(zhí)行。
計(jì)算機(jī)有單核和多核之分竞慢,單 CPU 計(jì)算機(jī)實(shí)際為宏觀上并行,微觀上串行朴读。就馮諾依曼原理來說盾舌,微觀上只有等待上一條指令執(zhí)行完畢才會執(zhí)行下一條指令,任意一個(gè)時(shí)刻只處理一條指令稽煤;而宏觀上來講核芽,我們眼睛所看到的是多個(gè)程序在“同時(shí)執(zhí)行”,這又是如何辦到呢酵熙?很簡單轧简,2 個(gè) 或 2 個(gè)以上的程序交替間隔得到 CPU 處理時(shí)間(稱之為時(shí)間片),給人造成 CPU 在同時(shí)處理多個(gè)程序的錯(cuò)覺匾二。
注意:概念中并發(fā)執(zhí)行的同一時(shí)間 4 個(gè)字可以從宏觀上理解哮独,當(dāng)然并發(fā)和并行是有一定區(qū)別的,具體請看Concurrency vs Parallelism 并發(fā)與并行一節(jié)察藐。
1.2 Synchronous vs. Asynchronous 同步 vs. 異步
概念:術(shù)語描述函數(shù)執(zhí)行某個(gè)任務(wù)后(任務(wù)交由 GCD 執(zhí)行)皮璧,等待計(jì)劃任務(wù)完成返回稱之為同步方式;而異步方式在將任務(wù)交由 GCD 執(zhí)行后立即返回分飞,執(zhí)行函數(shù)中余下的部分代碼悴务,若沒有則直接退出當(dāng)前函數(shù)。
注意:同步函數(shù)將會阻塞當(dāng)前線程譬猫,直到任務(wù)執(zhí)行完畢返回才進(jìn)行接下來的操作讯檐,而異步函數(shù)則不會,當(dāng)然它會很好地完成預(yù)定任務(wù)染服。
1.3 Critical Section 臨界區(qū)
概念:通過對多線程的串行化來訪問公共資源或一段代碼别洪,速度快,適合控制數(shù)據(jù)訪問柳刮。在任意時(shí)刻只允許一個(gè)線程對共享資源進(jìn)行訪問挖垛,如果有多個(gè)線程試圖訪問公共資源,那么在有一個(gè)線程進(jìn)入后诚亚,其他試圖訪問公共資源的線程將被掛起,并一直等到進(jìn)入臨界區(qū)的線程離開午乓,臨界區(qū)在被釋放后站宗,其他線程才可以搶占。
1.4 Race Condition 競態(tài)條件
概念:從多進(jìn)程間通信的角度來講益愈,是指兩個(gè)或多個(gè)進(jìn)程對共享的數(shù)據(jù)進(jìn)行讀或?qū)懙牟僮鲿r(shí)梢灭,最終的結(jié)果取決于這些進(jìn)程的執(zhí)行順序夷家。
多描述基于特定序列或事件執(zhí)行時(shí)機(jī)的軟件系統(tǒng)以不受控制的方式運(yùn)行的行為 ,例如程序的并發(fā)任務(wù)執(zhí)行的確切順序敏释。競態(tài)條件可導(dǎo)致無法預(yù)測的行為库快,而不能通過代碼檢查立即發(fā)現(xiàn)。
1.5 Deadlock 死鎖
概念:兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過程中钥顽,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象义屏,若無外力作用,它們都將無法推進(jìn)下去蜂大。此時(shí)稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖闽铐,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
簡單理解:兩個(gè)(有時(shí)更多)東西——在大多數(shù)情況下奶浦,是線程——所謂的死鎖是指它們都卡住了兄墅,并等待對方完成或執(zhí)行其它操作。第一個(gè)不能完成是因?yàn)樗诘却诙€(gè)的完成澳叉。但第二個(gè)也不能完成隙咸,因?yàn)樗诘却谝粋€(gè)的完成。
1.6 Thread Safe 線程安全
線程安全的代碼能在多線程或并發(fā)任務(wù)中被安全的調(diào)用成洗,而不會導(dǎo)致任何問題(數(shù)據(jù)損壞五督,崩潰,等)泌枪。線程不安全的代碼在某個(gè)時(shí)刻只能在一個(gè)上下文中運(yùn)行概荷。一個(gè)線程安全代碼的例子是 NSDictionary 。你可以在同一時(shí)間在多個(gè)線程中使用它而不會有問題碌燕。另一方面误证,NSMutableDictionary 就不是線程安全的,應(yīng)該保證一次只能有一個(gè)線程訪問它修壕。
1.7 Context Switch 上下文切換
概念:一個(gè)上下文切換指當(dāng)你在單個(gè)進(jìn)程里切換執(zhí)行不同的線程時(shí)存儲與恢復(fù)執(zhí)行狀態(tài)的過程愈捅。這個(gè)過程在編寫多任務(wù)應(yīng)用時(shí)很普遍,但會帶來一些額外的開銷慈鸠。
注意:前面提及單 CPU 計(jì)算機(jī)微觀上是串行執(zhí)行任務(wù)蓝谨,同一時(shí)刻只允許處理單個(gè)任務(wù),采用時(shí)間調(diào)度方式青团,從宏觀上給人造成多個(gè)程序同時(shí)處理的假象譬巫。而程序A 切換到程序 B 之時(shí)必須做一些工作:存儲當(dāng)前程序 A 的作業(yè)環(huán)境(eg.執(zhí)行到哪了?環(huán)境變量 etc.)督笆;恢復(fù)程序 B 的作業(yè)環(huán)境開始工作芦昔,即所謂的上下文切換。
1.8 Concurrency vs Parallelism 并發(fā)與并行
概念:并發(fā)和并行從宏觀角度來看都是同時(shí)處理多個(gè)任務(wù)娃肿。但并發(fā)和并行又有區(qū)別咕缎,如果你理解的同時(shí)是指同一個(gè)時(shí)刻發(fā)生珠十,那么稱之為兩個(gè)或多個(gè)任務(wù)并行執(zhí)行;若你理解的同時(shí)是指同一時(shí)間間隔(0.01秒內(nèi))發(fā)生凭豪,那么稱之為多個(gè)任務(wù)并發(fā)執(zhí)行焙蹭。
并發(fā)代碼的不同部分可以同時(shí)執(zhí)行,當(dāng)然嫂伞,至于怎么發(fā)生或是否發(fā)生都取決于系統(tǒng)孔厉。多核設(shè)備會開辟多個(gè)線程同時(shí)執(zhí)行代碼的不同部分,稱之為并行末早;然而烟馅,單核設(shè)備如上面所提及的,它只有一個(gè)“大腦”然磷,同一時(shí)刻只能執(zhí)行一項(xiàng)任務(wù)郑趁,想要實(shí)現(xiàn)一樣的效果,首先必須運(yùn)行一個(gè)線程姿搜,執(zhí)行上下文切換寡润,然后運(yùn)行另外一個(gè)線程或進(jìn)程,稱之為并發(fā)舅柜。這通常發(fā)生地足夠快以致給我們并發(fā)執(zhí)行地錯(cuò)覺梭纹,如下圖所示:
總結(jié):用數(shù)學(xué)上的集合符號表示并行 ∈ 并發(fā) ,GCD 中我們可以編寫代碼要求并發(fā)執(zhí)行致份,但 GCD 會為我們決定哪些代碼并行執(zhí)行卻是未知的变抽。并行執(zhí)行一定是并發(fā)執(zhí)行,而并發(fā)執(zhí)行不一定是并行執(zhí)行氮块,畢竟單核設(shè)備也能通過上下文切換绍载,造成多個(gè)任務(wù)“同時(shí)執(zhí)行”的假象。
更多:如果你想深入了解任務(wù)的并發(fā)執(zhí)行滔蝉,不妨看看 this excellent talk by Rob Pike击儡。
1.9 Queues 隊(duì)列
GCD 提供 dispatch queues ** 管理代碼塊。這些隊(duì)列通過 FIFO 方式執(zhí)行你提供給 GCD 的所有任務(wù)蝠引。FIFO** : First Input First Output 的縮寫阳谍,是一種傳統(tǒng)的按序執(zhí)行方式,這意味著第一個(gè)被添加到隊(duì)列里的任務(wù)將會是隊(duì)列中第一個(gè)開始的任務(wù)螃概,而第二個(gè)被添加到隊(duì)列的任務(wù)將會是第二個(gè)開始矫夯,同理隊(duì)列中其他任務(wù)也是如此。但是第二個(gè)任務(wù)何時(shí)開始我們不得而知吊洼,唯一能確定的是它將在第一個(gè)任務(wù)開始之后執(zhí)行训貌。
所有的調(diào)度隊(duì)列(dispatch queues)自身都是線程安全,你能同時(shí)在多個(gè)線程訪問它們融蹂。 GCD 的優(yōu)點(diǎn)顯而易見的旺订,前提是你必須了解調(diào)度隊(duì)列如何為你自己代碼的不同部分提供線程安全。選擇正確類型的調(diào)度隊(duì)列和調(diào)度函數(shù)來提交任務(wù)是至關(guān)重要的超燃。
GCD 提供了兩種調(diào)度隊(duì)列:串行隊(duì)列和并發(fā)隊(duì)列区拳。
1.9.1 Serial Queues 串行隊(duì)列
串行隊(duì)列中加入的任務(wù)一次有且僅有一個(gè)被執(zhí)行,只有當(dāng)前一個(gè)任務(wù)執(zhí)行完畢意乓,后一個(gè)任務(wù)才能開始樱调,至于什么時(shí)候開始,這取決于 GCD届良,如下圖所示:
這些任務(wù)的執(zhí)行時(shí)機(jī)受到 GCD 的控制笆凌;唯一能確保的事情是 GCD 一次只執(zhí)行一個(gè)任務(wù),并且按照我們添加到隊(duì)列的順序來執(zhí)行士葫。
由于在串行隊(duì)列中不會有兩個(gè)任務(wù)并發(fā)運(yùn)行乞而,因此不會出現(xiàn)同時(shí)訪問臨界區(qū)的風(fēng)險(xiǎn);相對于這些任務(wù)來說慢显,這就從競態(tài)條件下保護(hù)了臨界區(qū)爪模。所以如果訪問臨界區(qū)的唯一方式是通過提交到調(diào)度隊(duì)列的任務(wù),那么你就不需要擔(dān)心臨界區(qū)的安全問題了荚藻。
1.9.2 Concurrent Queues 并發(fā)隊(duì)列
并發(fā)隊(duì)列中的任務(wù)能得到的保證是它們會按照被添加的順序開始執(zhí)行屋灌,但這就是全部的保證了。任務(wù)可能以任意順序完成应狱,你不會知道何時(shí)開始運(yùn)行下一個(gè)任務(wù)共郭,或者任意時(shí)刻有多少 Block 在運(yùn)行。再說一遍疾呻,這完全取決于 GCD 除嘹。
下圖展示了一個(gè)示例任務(wù)執(zhí)行計(jì)劃,GCD 管理著四個(gè)并發(fā)任務(wù):
注意到隊(duì)列中 4 個(gè)任務(wù)執(zhí)行順序即為添加到隊(duì)列的順序罐韩,但是 Block1 并未在 Block0 開始后立即執(zhí)行憾赁,而是等待一段時(shí)間后開始(圖中是在 Block0 執(zhí)行完畢后開始),而 Block1散吵、Block2 和 Block3 按照順序立即執(zhí)行龙考。
何時(shí)開始一個(gè) Block 完全取決于 GCD 。如果一個(gè) Block 的執(zhí)行時(shí)間與另一個(gè)重疊矾睦,也是由 GCD 來決定是否分配一個(gè) CPU 核單獨(dú)處理晦款,否則就用上下文切換的方式來執(zhí)行不同的 Block 。
GCD 提供了至少五個(gè)特定的隊(duì)列枚冗,可根據(jù)隊(duì)列類型選擇使用缓溅。
1.9.3 Queue Types 隊(duì)列類型
隊(duì)列 | 隊(duì)列類型 | 說明 |
---|---|---|
主隊(duì)列(main queue) | 串行 | 保證所有的任務(wù)都在主線程執(zhí)行,而主線程是唯一用于 UI 更新的線程赁温。此外還用于發(fā)送消息給視圖或發(fā)送通知坛怪。 |
四個(gè)全局調(diào)度隊(duì)列(high淤齐、default、low袜匿、background) | 并發(fā) | Apple 的接口也會使用這些隊(duì)列更啄,所以你添加的任何任務(wù)都不會是這些隊(duì)列中唯一的任務(wù) |
自定義隊(duì)列 | 串行 or 并發(fā) | 1. 多個(gè)任務(wù)以串行方式執(zhí)行,但又不想在主線程中居灯;2. 多個(gè)任務(wù)以并行方式執(zhí)行祭务,但不希望隊(duì)列中有其他系統(tǒng)的任務(wù)干擾。 |
2 API 接口介紹
2.1 dispatch_sync
一般使用方式:
// 線程 A 調(diào)用 someMethod 方法
- (void)someMethod {
// 同步
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
// 由于是同步怪嫌,線程A會被阻塞
[self doOtherThing];
}
概述:線程 A 執(zhí)行 someMethod 方法义锥,將任務(wù) block 同步加入到隊(duì)列 queue 中等待執(zhí)行,由于是同步加入岩灭,表明阻塞線程 A(至于為什么阻塞拌倍,可以閱讀源代碼理解),只有 block 執(zhí)行完畢噪径,才能繼續(xù)執(zhí)行 doOtherThing 方法贰拿。
在繼續(xù)之后的 API 講解前,希望一定要理解:我們使用 GCD 接口僅涉及 Queue & Task熄云,正確地把 Task 加入到 Queue膨更,然后什么都不用管。而 GCD 所要做的工作正如它的名字:Grand Central Dispatch 任務(wù)派發(fā)缴允,根據(jù)任務(wù)性質(zhì)荚守,所處環(huán)境以及機(jī)器配置來決定是否使用現(xiàn)有線程,哪個(gè)線程练般,或是創(chuàng)建一個(gè)新的線程矗漾,然后把任務(wù)派發(fā)出去。如果是串行隊(duì)列薄料,它包含多個(gè)任務(wù)敞贡,將任務(wù)按照 FIFO 原則派發(fā)到同一個(gè)線程中執(zhí)行,至于哪個(gè)線程摄职,前面說了視情況而定誊役;如果是并發(fā)隊(duì)列,依舊按照 FIFO 原則派發(fā)都不同的線程中執(zhí)行谷市。
Q:并發(fā)隊(duì)列是指將每個(gè)任務(wù)都放到不同線程去執(zhí)行嗎蛔垢?
這里給出的是raywenderlich的gif講解:
2.1 dispatch_async
一般使用方式:
// 線程 A 調(diào)用 someMethod 方法
- (void)someMethod {
// 異步
dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
// 由于是異步,線程A不會被阻塞
[self doOtherThing];
}
概述:線程 A 執(zhí)行 someMethod 方法迫悠,將任務(wù) block 異步加入到隊(duì)列 queue 中等待執(zhí)行鹏漆,由于是異步加入,線程A不會阻塞,會立即執(zhí)行 doOtherThing 方法艺玲。至于加入到隊(duì)列的 block括蝠,正排著隊(duì),等 GCD 分配呢饭聚!
同樣給出的是raywenderlich的gif講解:
2.2 dispatch_after
一般使用方式:
// 線程 A 調(diào)用 someMethod 方法
- (void)someMethod {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
<#code to be executed after a specified delay#>
});
}
概述:
dispatch_after
不管 someMethod 方法在哪個(gè)線程被調(diào)用又跛,代碼的意思為將任務(wù) block 加入到主隊(duì)列中——此時(shí)沒有把任務(wù)派發(fā)到主線程中去執(zhí)行,而是等待delayInSeconds秒才去執(zhí)行若治,另外也不會阻塞someMethod方法,這一點(diǎn)和dispatch_async
一樣感混,其實(shí)它更像是一個(gè)延遲的dispatch_async
端幼。
Q:將任務(wù)加入到主隊(duì)列中,如何實(shí)現(xiàn)一定時(shí)間后在主線程中執(zhí)行block任務(wù)?
2.3 dispatch_once
一般使用方式:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
<#code to be executed once#>
});
概述:這里不局限于單例生成弧满,能夠保證多線程情況下婆跑,代碼只執(zhí)行一次!請回顧下 Critical Section 臨界區(qū)小節(jié)內(nèi)容庭呜,是的滑进!沒錯(cuò),這份代碼就是臨界區(qū)內(nèi)容啦募谎,只允許一個(gè)線程訪問扶关!其他線程只能干看著,而執(zhí)行過一次后数冬,我們會設(shè)置靜態(tài)變量onceToken來標(biāo)識已經(jīng)執(zhí)行過了节槐,就算輪到下一個(gè)線程訪問也不會執(zhí)行了。
2.4 dispatch_barrier_sync
和 dispatch_barrier_async
首先 sync 和 async 的區(qū)別在于是否會阻塞當(dāng)前線程拐纱,因此這里我們更加關(guān)注 barrier 的作用:
// 線程 A 調(diào)用 someMethod 方法
- (void)someMethod {
// 同步
dispatch_barrier_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
// 由于是同步铜异,線程A會被阻塞
[self doOtherThing];
}
可以看到調(diào)用方法和 dispatch_sync
以及 dispatch_async
其實(shí)是一致的,無非就是將任務(wù)block放入隊(duì)列queue等待被執(zhí)行秸架,回顧下:dispatch_sync
會阻塞當(dāng)前線程揍庄,直到隊(duì)列中的任務(wù)被分派到某個(gè)線程被執(zhí)行完畢;而dispatch_async
則不用等待任務(wù)完成东抹,不阻塞當(dāng)前線程蚂子,直接執(zhí)行當(dāng)前之后的任務(wù)$郧可以看到由這兩個(gè)方法加入隊(duì)列的任務(wù)缆镣,只是遵循了FIFO順序執(zhí)行,而其他執(zhí)行要求則沒有了试浙。
概述:是時(shí)候說說 barrier 了董瞻,由它加入隊(duì)列的任務(wù)(先稱為A)也同樣遵循FIFO順序執(zhí)行,但是重點(diǎn)來了,任務(wù)A會等待它之前的所有其他任務(wù)完成钠糊,才開始執(zhí)行挟秤!而它之后的任務(wù)會暫停,等待任務(wù)A完成之后繼續(xù)按照之后順序來抄伍。其他barrier方式加入隊(duì)列的任務(wù)都是如此艘刚。
摘自raywenderlich的解釋圖:
剛才解釋了Barrier的作用,試想下串行隊(duì)列和并發(fā)隊(duì)列:串行隊(duì)列中的任務(wù)都是一個(gè)接一個(gè)的執(zhí)行截珍,那barrier貌似多此一舉了攀甚!再想想并發(fā)隊(duì)列,我們希望block0 block1 block2 block3 被分發(fā)到不同的線程并發(fā)執(zhí)行岗喉,而根據(jù)FIFO的順序輪到 BarrierBlock時(shí)秋度,它會等待那四個(gè)家伙執(zhí)行完畢,最晚的那個(gè)執(zhí)行完畢就開始執(zhí)行BarrierBlock钱床,而隊(duì)列中它后面的任務(wù)都會暫停(正常的會繼續(xù)將任務(wù)分派到線程中)荚斯。以大菊官來說,有點(diǎn)像串行方式查牌,(Block 0 Block1 block2 block3)組成一個(gè)整體事期,Barrier Block獨(dú)立一個(gè),(Block 5 Block 6)一個(gè)整體纸颜!三者之間執(zhí)行的順序是串行的兽泣!
2.5 Dispatch Groups
這里引入“組(group)”的概念,與隊(duì)列不同胁孙,任何加入到組中的任務(wù)(task)撞叨,可以是串行執(zhí)行或并行執(zhí)行,可以來自任何其他隊(duì)列浊洞,當(dāng)組中所有任務(wù)完成之時(shí)牵敷,會通知你這個(gè)消息。下面是幾個(gè)常用接口:
-
dispatch_group_t group_name = dispatch_group_create();
實(shí)例化一個(gè)組 -
dispatch_group_enter(<#dispatch_group_t _Nonnull group#>)
和dispatch_group_leave(<#dispatch_group_t _Nonnull group#>)
法希,“加入”和“離開”是一對枷餐,就好比Objective-C 內(nèi)存管理一樣,誰持有(retain
)誰釋放(release
) -
dispatch_group_wait(<#dispatch_group_t _Nonnull group#>,DISPATCH_TIME_FOREVER)
阻塞當(dāng)前線程苫亦,等待任務(wù)組中的所有任務(wù)執(zhí)行完畢毛肋。 -
dispatch_group_notify(<#dispatch_group_t _Nonnull group#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
和3不同,當(dāng)組中的全部執(zhí)行完畢屋剑,將block
任務(wù)加入到隊(duì)列queue
執(zhí)行润匙。
1. wait 阻塞等待方式:
- (void)doSomething:(NSURL *)url{
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_enter(downloadGroup); // 1
[Server downloadSomethingWithURL:url
withCompletionBlock:^(NSString *result, NSError *error){
// 抓取到數(shù)據(jù) 可以做一些解析工作
dispatch_group_leave(downloadGroup); // 2
}];
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 3
// 做其他一些事情
}
- 你可以將
group
當(dāng)做一個(gè)計(jì)數(shù)器,在開始你的任務(wù)之前進(jìn)行enter
操作(組中任務(wù)數(shù)量 +1 )唉匾; - 之前說了有進(jìn)就有出孕讳,所以當(dāng)你完成任務(wù)時(shí)要及時(shí)退出來進(jìn)行平衡——(組中任務(wù)數(shù)量-1);
-
wait
的地方會一直等待group
組中的任務(wù)數(shù)量歸零匠楚,這里DISPATCH_TIME_FOREVER
表示一直等待,阻塞當(dāng)前線程厂财。
2. notify 通知響應(yīng)方式:
- (void)doSomething:(NSURL *)url{
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_enter(downloadGroup);
[Server downloadSomethingWithURL:url
withCompletionBlock:^(NSString *result, NSError *error){
// 抓取到數(shù)據(jù) 可以做一些解析工作
dispatch_group_leave(downloadGroup); /
}];
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
// 當(dāng)組中的所有任務(wù)完成時(shí) 會通知主隊(duì)列執(zhí)行這個(gè)閉包
});
// 這里不會阻塞線程 會立馬執(zhí)行
}
使用 notify
方式不會阻塞當(dāng)前線程芋簿,而是等待組中所有任務(wù)完成,通知指定隊(duì)列(這里是main queue)去執(zhí)行閉包璃饱。
2.6 dispatch_apply
dispatch_apply
可以認(rèn)為是并發(fā)方式的 for
循環(huán)語句与斤,接口完整定義如下:
dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t _Nonnull queue#>, <#^(size_t)block#>)
使用方法:
// 串行方式做事情
- (void)serialDoSomething {
for(int idx=0; idx < 3; idx++) {
// 這里你可以處理事情 比如下載圖片
downloadPic(idx);
}
}
// 并發(fā)方式做事情
- (void)concurrencyDoSomething {
dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t idx) {
// 由于下載圖片之間沒有任何關(guān)系,允許并發(fā)的去下載
downloadPic(idx);
})
}
2.7 Semaphores 信號量
日常開發(fā)中荚恶,我們對串行執(zhí)行方式“愈加不滿”撩穿,不斷開辟線程來處理事務(wù),要知道線程達(dá)到一定數(shù)量會導(dǎo)致應(yīng)用崩潰谒撼!因此一方面我們希望并發(fā)處理食寡,一方面又不想過多的創(chuàng)建線程(可能是無心之失,執(zhí)行任務(wù)過于耗時(shí)嗤栓,不斷累積導(dǎo)致最后線程數(shù)量爆炸)。
因此我們需要信號量來控制并發(fā)操作箍邮。dispatch_semaphore_create(count)
創(chuàng)建一個(gè)初始值為 count
的信號量茉帅,允許訪問資源的總量(這里的資源就是線程數(shù)量),使用 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
查詢是否有足夠的資源供當(dāng)前使用锭弊,當(dāng)信號總量小于等于0的時(shí)候會一直等待堪澎,否則表示有足夠的資源(起碼有一個(gè)),允許執(zhí)行你要的操作味滞,并讓信號總量減 1 ——因?yàn)榇丝棠阏加辛怂8颉.?dāng)然使用完這個(gè)資源時(shí),你需要使用 dispatch_semaphore_signal(semaphore)
來通知信號量加 1來 來釋放資源使用權(quán)剑鞍。其他等待信號量大于 0 的地方昨凡,此刻由于資源的占有權(quán)空出,允許開始執(zhí)行他們的任務(wù)了蚁署。
結(jié)合之前的學(xué)習(xí)便脊,看下 group
結(jié)合信號量的使用方式:
dispatch_group_t group = dispatch_group_create(); // 1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10); // 2
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 3
for (int i = 0; i < 100; i++)
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 4
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i); // 5
sleep(2);
dispatch_semaphore_signal(semaphore); // 6
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);// 7
NSLog(@"所有任務(wù)完成");
- 創(chuàng)建一個(gè)任務(wù)組
- 創(chuàng)建一個(gè)總量為10的信號量
- 這里獲取低優(yōu)先級的全局隊(duì)列,注意是并發(fā)的光戈,它會將隊(duì)列中的任務(wù)派發(fā)到不同線程并發(fā)執(zhí)行
- 查詢當(dāng)前是否有足夠資源可供使用哪痰,換句話說信號量是否大于0,有則執(zhí)行下面的語句久妆,信號量減1晌杰,否則阻塞當(dāng)前線程等待
- 這里使用 sleep 來模擬耗時(shí)的任務(wù)
- 執(zhí)行完任務(wù)后釋放掉占有的資源,即對信號量進(jìn)行加1操作
- 等待組中所有任務(wù)執(zhí)行完畢
3 場景應(yīng)用
3.1 匯總
編號 | 場景描述 |
---|---|
場景一 | 數(shù)據(jù)分別來自兩個(gè)沒有直接聯(lián)系的網(wǎng)絡(luò)請求筷弦,當(dāng)且僅當(dāng)獲取到完整數(shù)據(jù)時(shí)進(jìn)行UI刷新 |
3.2 場景一
場景描述:
數(shù)據(jù)分別來自兩個(gè)沒有直接聯(lián)系的網(wǎng)絡(luò)請求肋演,當(dāng)且僅當(dāng)獲取到完整數(shù)據(jù)時(shí)進(jìn)行UI刷新
這個(gè)場景的解決思路有多種:
- 使用同步的請求方法,意味著發(fā)出第一個(gè)請求后等待數(shù)據(jù)返回,然后發(fā)送第二個(gè)請求等待數(shù)據(jù)返回惋啃,最后進(jìn)行 UI 刷新哼鬓。但是平常開發(fā)中我們用到的請求接口通常是異步的,很少用諸如
[NSString stringWithContentsOfURL:...]
此類同步接口 (為了不阻塞主線程边灭,我們可能會開辟一個(gè)新線程放到后臺去執(zhí)行請求行為)异希。 - 采用異步方式的請求,但是定義變量來標(biāo)識請求回調(diào)情況绒瘦,這里我說兩種方法:1. 本文的場景我們可以定義兩個(gè)
BOOL
類型的變量分別標(biāo)識請求回調(diào)是否到來称簿,兩個(gè)請求的回調(diào)中都有一個(gè)if(isFinishedRequestA && isFinishedRequestB){ [self updateUI];}
條件語句,這里有個(gè)弊端惰帽,如果有3個(gè)請求憨降,或者4個(gè),5個(gè)?… 2. 定義一個(gè)計(jì)數(shù)器——相當(dāng)簡單int finishRequestCounter = 0
该酗,請求1和請求2只要數(shù)據(jù)到來授药,計(jì)數(shù)器計(jì)數(shù)+1,你可以使用KVO
監(jiān)聽計(jì)數(shù)器數(shù)值的變化呜魄,當(dāng)然也可以重寫其setter
方法悔叽,一旦數(shù)值大于等于2,表明兩個(gè)請求都回調(diào)成功了爵嗅,進(jìn)行 UI 刷新娇澎。思考下:我們可能會在兩個(gè)線程同時(shí)修改計(jì)數(shù)器,這會有什么問題睹晒? - 使用
group
任務(wù)組的方式(2.5 小節(jié)介紹的Dispatch Groups
)趟庄,它用起來很像第二種方案的計(jì)數(shù)器,下面我們通過偽代碼來講解:
- (void)doSomeInit {
_downloadGroup = dispatch_group_create(); // 1
...
}
- (void) requestA {
dispatch_group_enter(downloadGroup); // 2
[Server requestWithParameter:parameterForA withCompletionBlock:^(NSString *result, NSError *error){
// 回調(diào) 你可以做一些事情
dispatch_group_leave(downloadGroup); // 3
}];
}
- (void) requestB {
dispatch_group_enter(downloadGroup); // 2
[Server requestWithParameter:parameterForB withCompletionBlock:^(NSString *result, NSError *error){
// 回調(diào) 你可以做一些事情
dispatch_group_leave(downloadGroup); // 3
}];
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
[self updateUI]; // 4
});
}
-
_downloadGroup
的類型為dispatch_group_t
伪很,加入到組中的任務(wù)并不限制為來自同一個(gè)隊(duì)列戚啥,允許不同隊(duì)列的任務(wù)加入其中 —— 兩個(gè)接口請看點(diǎn) 2 和 3 。請牢記:使用 GCD 意味著你將更多關(guān)注于隊(duì)列锉试,而非線程虑鼎,因?yàn)槿蝿?wù)派發(fā)到哪個(gè)線程,是否新起一個(gè)線程都是由 GCD 負(fù)責(zé)的键痛,你不需要關(guān)心這些 -
requestA
和requestB
對于組的操作實(shí)際是一樣的炫彩,因此這里統(tǒng)一標(biāo)注成 2 和 3 個(gè)點(diǎn)。前面說到dispatch_group_t
組很像一個(gè)計(jì)數(shù)器絮短,比如調(diào)用dispatch_group_enter(downloadGroup)
告訴downloadGroup
組加入了某個(gè)任務(wù)江兢,請計(jì)數(shù)+1
,至于加入了什么任務(wù)丁频,在哪里加的杉允?實(shí)際上并不知曉邑贴,這一切都靠我們自己來把控 —— 這里我們放在請求前。你可能疑惑這句代碼在哪個(gè)線程被調(diào)用有關(guān)系嗎叔磷?如果多個(gè)線程同時(shí)加任務(wù)呢拢驾?答案是線程安全。 - 有了上面的鋪墊改基,
dispatch_group_leave(downloadGroup);
告訴downloadGroup
組某個(gè)任務(wù)已經(jīng)完成了繁疤,需要計(jì)數(shù)-1
,至于它在哪個(gè)隊(duì)列秕狰,哪些線程完成稠腊,我們都不關(guān)心。注意點(diǎn):enter
和leave
是一對操作鸣哀,就像ARC
內(nèi)存管理中的retain
和release
一樣彪杉,誰創(chuàng)建誰負(fù)責(zé)釋放靡狞,這是一種平衡粤咪。而這種平衡行為需要我們自己手動去管理斋日,方法requestA
中,我們希望在請求“前一瞬間”告訴組有個(gè)任務(wù)要加入其中挠羔,所以才有了dispatch_group_enter
井仰,而下一刻調(diào)用[Server requestWithParameter:...
發(fā)起請求,這兩句代碼處于同一個(gè)線程且按序執(zhí)行褥赊,因此這么寫沒有任何問題糕档,而當(dāng)回調(diào)到來處理一些基本事務(wù)莉恼,比如解析數(shù)據(jù)后拌喉,標(biāo)識之前加入到組中的任務(wù)被執(zhí)行完畢——實(shí)際上怎么算任務(wù)執(zhí)行完畢取決于你。 - 這里我們沒有使用
dispatch_group_wait
阻塞等待俐银,而是選擇了一種更合理的方式尿背,主動等待誰都不喜歡,還不如當(dāng)組中的所有任務(wù)完成時(shí)(可以認(rèn)為計(jì)數(shù)器為0的時(shí)候)通知我們捶惜。由于 UI 的更新行為一定要處于主線程田藐,所以在被通知時(shí)候,我們會將刷新UI操作的閉包加入到主隊(duì)列中吱七。注意: 這段代碼放在requestA
的下方還是requestB
的下方取決于你的代碼癖好汽久。
課后作業(yè):
問題一:
假設(shè)同一個(gè)線程下先調(diào)用 requestA
,然后調(diào)用 requestB
踊餐,此時(shí)使用 dispatch_group_wait
替換上面的 dispatch_group_notify
會怎么樣呢景醇?請求調(diào)用順序換一下會發(fā)生什么事情?
問題二:
- (void)doSomething{
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSLog(@"xxxxxxx");
});
NSLog(@"yyyyyy");
}
沒有 enter
和 leave
操作吝岭,這里會被 notify
嗎三痰?輸出是什么吧寺?
問題三:
dispatch_group_notify
這段代碼被執(zhí)行到之前,請求A和B的回調(diào)居然都回來了散劫,此時(shí)會怎么樣稚机?ps:可以結(jié)合問題二思考。