Grand Central Dispatch(GCD) 深入淺出

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_syncdispatch_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
  // 做其他一些事情
}
  1. 你可以將group當(dāng)做一個(gè)計(jì)數(shù)器,在開始你的任務(wù)之前進(jìn)行enter操作(組中任務(wù)數(shù)量 +1 )唉匾;
  2. 之前說了有進(jìn)就有出孕讳,所以當(dāng)你完成任務(wù)時(shí)要及時(shí)退出來進(jìn)行平衡——(組中任務(wù)數(shù)量-1);
  3. 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ù)完成");
  1. 創(chuàng)建一個(gè)任務(wù)組
  2. 創(chuàng)建一個(gè)總量為10的信號量
  3. 這里獲取低優(yōu)先級的全局隊(duì)列,注意是并發(fā)的光戈,它會將隊(duì)列中的任務(wù)派發(fā)到不同線程并發(fā)執(zhí)行
  4. 查詢當(dāng)前是否有足夠資源可供使用哪痰,換句話說信號量是否大于0,有則執(zhí)行下面的語句久妆,信號量減1晌杰,否則阻塞當(dāng)前線程等待
  5. 這里使用 sleep 來模擬耗時(shí)的任務(wù)
  6. 執(zhí)行完任務(wù)后釋放掉占有的資源,即對信號量進(jìn)行加1操作
  7. 等待組中所有任務(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
  });
}
  1. _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)心這些
  2. requestArequestB 對于組的操作實(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ù)呢拢驾?答案是線程安全。
  3. 有了上面的鋪墊改基,dispatch_group_leave(downloadGroup); 告訴 downloadGroup 組某個(gè)任務(wù)已經(jīng)完成了繁疤,需要計(jì)數(shù) -1,至于它在哪個(gè)隊(duì)列秕狰,哪些線程完成稠腊,我們都不關(guān)心。注意點(diǎn): enterleave 是一對操作鸣哀,就像 ARC 內(nèi)存管理中的 retainrelease 一樣彪杉,誰創(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í)行完畢取決于你。
  4. 這里我們沒有使用 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");
}

沒有 enterleave 操作吝岭,這里會被 notify 嗎三痰?輸出是什么吧寺?

問題三:

dispatch_group_notify 這段代碼被執(zhí)行到之前,請求A和B的回調(diào)居然都回來了散劫,此時(shí)會怎么樣稚机?ps:可以結(jié)合問題二思考。

4 源碼分析

文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末获搏,一起剝皮案震驚了整個(gè)濱河市赖条,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颜凯,老刑警劉巖谋币,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異症概,居然都是意外死亡蕾额,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門彼城,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诅蝶,“玉大人,你說我怎么就攤上這事募壕〉骶妫” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵舱馅,是天一觀的道長缰泡。 經(jīng)常有香客問我,道長代嗤,這世上最難降的妖魔是什么棘钞? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮干毅,結(jié)果婚禮上宜猜,老公的妹妹穿的比我還像新娘。我一直安慰自己硝逢,他們只是感情好姨拥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渠鸽,像睡著了一般叫乌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徽缚,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天憨奸,我揣著相機(jī)與錄音,去河邊找鬼猎拨。 笑死膀藐,一個(gè)胖子當(dāng)著我的面吹牛屠阻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播额各,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼国觉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了虾啦?” 一聲冷哼從身側(cè)響起麻诀,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎傲醉,沒想到半個(gè)月后蝇闭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡硬毕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年呻引,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吐咳。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逻悠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出韭脊,到底是詐尸還是另有隱情童谒,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布沪羔,位于F島的核電站饥伊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蔫饰。R本人自食惡果不足惜琅豆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望死嗦。 院中可真熱鬧趋距,春花似錦粒氧、人聲如沸越除。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘盆。三九已至,卻和暖如春饱苟,著一層夾襖步出監(jiān)牢的瞬間孩擂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工箱熬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留类垦,地道東北人狈邑。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像蚤认,于是被迫代替她去往敵國和親米苹。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容