Grand Central Dispatch的使用

在計算機的早期,中央處理器(Central Processing Unit棠笑,簡稱CPU)的主頻(即時鐘頻率辉词, clock speed )決定單位時間可執(zhí)行任務(wù)數(shù)量。隨著技術(shù)的進步闸度,處理器的設(shè)計變得更加緊湊竭贩,散熱和其他物理因素開始限制CPU的時鐘頻率。因此莺禁,芯片(chip)制造商尋求其他途徑來提高芯片的整體性能留量,最終解決方案就是增加芯片上處理器的內(nèi)核數(shù)。通過增加內(nèi)核數(shù)量哟冬,單個芯片每秒可執(zhí)行更多指令(instruction)楼熄,而不需要增加CPU主頻、改變芯片大小柒傻。唯一的問題是如何利用額外的核心孝赫。

對于像macOS、iOS這樣的多任務(wù)操作系統(tǒng)红符,任一時刻都會有上百個程序在運行青柄。因此,需要安排不同程序在不同核心上预侯。然而致开,大部分正在運行的程序要么是系統(tǒng)守護進程( system daemon)要么是后臺應(yīng)用,只需要占用非常短的處理器時間萎馅。相反双戳,用戶當前使用的app,占用CPU主要資源糜芳。

應(yīng)用程序使用多核的傳統(tǒng)方式是創(chuàng)建多個線程(multiple thread)飒货,但線程代碼最大問題是不能很好擴展到任意數(shù)量的內(nèi)核。你不能創(chuàng)建與內(nèi)核一樣多的線程峭竣,并期望程序運行良好塘辅。系統(tǒng)當前負載和硬件共同決定當前最佳線程數(shù)量,即當前最佳線程數(shù)量會動態(tài)變化皆撩。因此扣墩,計算出當前最佳線程數(shù)量將非常具有挑戰(zhàn)性。即使你的計算是正確的,使這些線程高效運行呻惕、互不干擾也是非常具有挑戰(zhàn)性的补鼻。

macOS和iOS不依賴線程浅侨,而是采用了異步(asynchronous)設(shè)計方案來解決并發(fā)(concurrency)問題早直。異步函數(shù)已在操作系統(tǒng)中使用了很多年嘹裂,通常用于處理耗時的任務(wù),如讀取磁盤數(shù)據(jù)型酥。異步函數(shù)調(diào)用任務(wù)后立即返回山憨,不需要等待任務(wù)執(zhí)行查乒。通常弥喉,這項工作包括以下三部分:

  1. 獲取后臺線程。
  2. 在獲取到的后臺線程開始任務(wù)玛迄。
  3. 當任務(wù)執(zhí)行完畢時通過回調(diào)通知調(diào)用者由境。

在過去,如果沒有滿足需求的后臺線程蓖议,就需要自己編寫異步函數(shù)虏杰,創(chuàng)建、管理后臺線程勒虾。這類多線程方案如下:

  • pthread:是一套通用的多線程API纺阔,由C語言編寫。適用于Unix修然、Linux笛钝、Windows等系統(tǒng),具有跨平臺愕宋、可移植優(yōu)點玻靡,但使用難度大,需開發(fā)者管理線程生命周期中贝,平常幾乎不使用囤捻。
  • NSThread:由 Objective-C 語言編寫。使用簡單邻寿,更加面向?qū)ο笮粒芍苯硬僮骶€程對象。生命周期由開發(fā)者控制绣否,偶爾使用NSThread誊涯。

現(xiàn)在,macOS和iOS提供以下技術(shù)支持異步執(zhí)行任何任務(wù)枝秤,而不需要自己管理線程醋拧。

  • Grand Central Dispatch簡稱GCD ,GCD是基于C語言開發(fā)的,其結(jié)合了語言特點丹壕、運行時庫(runtime library)庆械,可以為macOS、iOS菌赖、watchOS和tvOS系統(tǒng)上的多核設(shè)備提供系統(tǒng)級的并發(fā)支持缭乘,可以更好協(xié)調(diào)所有正在運行app的需求,并以均衡的方式分配可用資源琉用。使用GCD時應(yīng)用程序只需要定義需要執(zhí)行的任務(wù)堕绩,然后交由系統(tǒng)執(zhí)行,而不再需要創(chuàng)建線程邑时。通過讓系統(tǒng)管理線程奴紧,app獲得了原線程編程方式無法實現(xiàn)的伸縮性,開發(fā)人員也可以實現(xiàn)更簡單晶丘、更高效的編程模型黍氮。
  • 操作隊列(Operation Queue)是Objective-C API,是在GCD之上實現(xiàn)了一些方便的功能浅浮,即NSOperationAPI是GCD的高級抽象沫浆。如果你在使用NSOperation,那么你在隱式使用GCD滚秩。使用NSOperation時只需要定義要執(zhí)行的任務(wù)专执,將其添加到NSOperationQueue隊列即可,NSOperationQueue負責這些任務(wù)的調(diào)度和執(zhí)行郁油。與GCD一樣本股,NSOperationQueue負責所有的線程管理,以確保系統(tǒng)已艰、app盡可能高效的運行痊末。NSOperation是基類,不能直接使用哩掺,需繼承后使用其子類凿叠,或使用系統(tǒng)提供的子類來執(zhí)行任務(wù)。例如NSInvocationOperationNSBlockOperation嚼吞。如需了解更多盒件,可以查看Operation、OperationQueue的使用舱禽。

這一篇文章只涉及GCD的使用炒刁。

1. GCD術(shù)語

在學習GCD之前,需要學習幾個與此相關(guān)的概念誊稚。

1.1 串行Serial

任務(wù)串行執(zhí)行時翔始,一次只能執(zhí)行一個任務(wù)罗心。

在應(yīng)用程序中,術(shù)語任務(wù)(task)有廣泛的應(yīng)用城瞎,但在這篇文章中渤闷,可以把一個任務(wù)理解為一個塊(Block)。在函數(shù)中也可以使用GCD脖镀,但因塊更為簡單飒箭、直觀,所以蜒灰,更常使用塊弦蹂。

1.2 并發(fā)Concurrent

并發(fā)指task同時(simultaneously)發(fā)生,可以被同時執(zhí)行强窖,但具體是否同時執(zhí)行由GCD根據(jù)當前負載決定凸椿。

1.3 并行Parallelism

并行指多個任務(wù)同時執(zhí)行。

通過并行技術(shù)毕骡,多核設(shè)備可以同時執(zhí)行多個線程削饵;單核設(shè)備如果想要并行執(zhí)行任務(wù)岩瘦,就需要運行一個線程未巫,執(zhí)行環(huán)境切換(也稱上下文切換,context switch)启昧,運行另外一個線程或進程(process)叙凡,這通常很快發(fā)生,足以形成并行執(zhí)行的錯覺密末。如下圖所示:

Concurrency VS Parallelism

比如很多人同時在候車室排隊檢票握爷,可以理解為并發(fā)。如果候車室開通多個檢票口同時檢票严里,這時一次可為多位旅客檢票新啼,可理解為并行。

并行的反義詞是串行刹碾,并發(fā)實際上是關(guān)于結(jié)構(gòu)燥撞。當使用GCD編寫代碼時,需要構(gòu)建可以同時運行的任務(wù)迷帜,不能同時運行的任務(wù)物舒。可以同時運行的任務(wù)是否并行戏锹,及并行時同時執(zhí)行幾項任務(wù)由GCD來決定冠胯。并行的任務(wù)必須并發(fā),但并發(fā)的任務(wù)并不保證得到并行锦针。

1.4 同步Synchronous

同步執(zhí)行會等待提交的task執(zhí)行完畢后荠察,再繼續(xù)執(zhí)行后面任務(wù)置蜀。我們平常調(diào)用的方法都是同步方法,如第一行調(diào)用doSomething:方法悉盆,程序執(zhí)行第二行時盾碗,doSomething:方法肯定執(zhí)行完畢了。同步執(zhí)行一些耗時操作時會堵塞當前線程舀瓢。調(diào)用dispatch_sync函數(shù)并將任務(wù)提交到當前隊列(即dispatch_sync函數(shù)所在隊列)會導(dǎo)致死鎖廷雅。

因為是同步調(diào)用,目標隊列不會復(fù)制塊(Block_copy)京髓,而只引用塊航缀。為提高性能,同步函數(shù)一般盡可能在當前線程調(diào)用塊堰怨。

1.5 異步Asynchronous

異步指調(diào)用任務(wù)后立即返回芥玉,不需要等待任務(wù)執(zhí)行。異步不會堵塞當前線程备图。

1.6 臨界區(qū)塊Critical Section

Critical section的代碼不能被同時執(zhí)行灿巧,即不同線程、進程同時訪問揽涮。通常是因為代碼操縱共享資源抠藕,如果被多線程同時訪問,會損壞數(shù)據(jù)蒋困。如對NSMutableArray進行寫操作時盾似,不能同時進行讀取操作。

1.7 竟態(tài)條件Race Condition

在race condition條件下雪标,軟件系統(tǒng)的行為取決于任務(wù)的執(zhí)行順序零院,而任務(wù)執(zhí)行順序不受控。Race condition可能產(chǎn)生不可預(yù)知行為村刨,且通過代碼檢查時不易顯現(xiàn)出來告抄。

1.8 死鎖Dead Lock

兩個或多個任務(wù)、線程都在等待對方完成操作嵌牺。第一個任務(wù)無法完成打洼,因為它在等待第二個任務(wù)完成。第二個任務(wù)無法完成髓梅,因為它在等待第一個任務(wù)完成拟蜻。兩個任務(wù)相互等待,這就形成了死鎖枯饿。

1.9 線程安全Thread Safe

線程安全的代碼可以在多個線程或并發(fā)任務(wù)中同時執(zhí)行酝锅。非線程安全的代碼一次只能在一個環(huán)境中運行。例如奢方,NSArray是線程安全的搔扁,可以同時在多個線程或并發(fā)任務(wù)中執(zhí)行爸舒。而NSMutableArray不是線程安全的,一次只能在一個環(huán)境中運行稿蹲。

1.10 環(huán)境切換Context Switch

Context switch指當在同一個處理器處理不同線程時扭勉,存儲和恢復(fù)執(zhí)行狀態(tài)的過程。編寫多任務(wù)應(yīng)用程序時苛聘,進行上下文切換非常常見涂炎,但會產(chǎn)生額外的開銷。

2. GCD隊列

GCD提供的調(diào)度隊列( dispatch queue)是一個類似于對象的結(jié)構(gòu)设哗,用于管理向其提交的任務(wù)唱捣。所有的dispatch queue都是先進先出(first in, first out. 簡稱FIFO)的數(shù)據(jù)結(jié)構(gòu)。因此网梢,隊列中任務(wù)運行順序與添加順序一致震缭,即第一個進入隊列的任務(wù),第一個被執(zhí)行战虏;第二個進入隊列的任務(wù)拣宰,第二個被執(zhí)行;以此類推烦感。

所有dispatch queue自身都是線程安全的巡社,因此,你可以從多個線程訪問它們啸盏。當你了解了調(diào)度隊列如何為你編寫的代碼提供線程安全時重贺,GCD的好處就顯而易見了。使用GCD的關(guān)鍵在于把要執(zhí)行的任務(wù)提交到正確的dispatch queue或調(diào)度函數(shù)回懦。

GCD的隊列分為串行隊列(Serial Queues)和并發(fā)隊列(Concurrent Queues)兩種。

2.1 串行隊列Serial Queues

在Serial Queues中次企,一次執(zhí)行一個任務(wù)怯晕,一個任務(wù)完成后另一個任務(wù)才會開始。兩個任務(wù)間隔時間無法確定缸棵。

SerialQueues

串行隊列中任務(wù)執(zhí)行時間由GCD控制舟茶,唯一可以保證的是任務(wù)執(zhí)行順序和添加順序一致。

因為serial queues中任務(wù)一次只能運行一個堵第,所以不會產(chǎn)生同時訪問critical section吧凉,產(chǎn)生race condition的風險。

2.2 并發(fā)隊列Concurrent Queues

GCD只能保證并發(fā)隊列中的任務(wù)按照添加的順序開始執(zhí)行踏志。至于task結(jié)束順序阀捅,兩個task間時間間隔,任一時間正在運行task數(shù)量都無法保證针余。

Concurrent Queues

上圖中的Block 0開始一段時間后Block 1才開始執(zhí)行饲鄙,Block 1凄诞、Block 2、Block 3開始時間相差很短忍级。雖然Block 3比Block 2開始的晚帆谍,但結(jié)束的早。

GCD決定什么時間開始執(zhí)行任務(wù)轴咱。如果兩項任務(wù)執(zhí)行時間重合汛蝙,由GCD決定是否在另一個內(nèi)核上(如果目前有空閑內(nèi)核)運行任務(wù),還是執(zhí)行context switch來執(zhí)行另一任務(wù)朴肺。

3. 創(chuàng)建和管理Dispatch Queues

在提交任務(wù)到dispatch queue之前患雇,必須確定要使用的隊列類型以及如何使用。如果有特殊用途宇挫,可以自定義配置隊列苛吱。

為了方便使用,GCD默認提供了主隊列dispatch_get_main_queue和全局隊列dispatch_get_global_queue器瘪。

3.1 獲取主隊列dispatch_get_main_queue

主隊列是一個全局的串行隊列翠储,運行在應(yīng)用程序的主線程上。主隊列與run loop協(xié)同工作橡疼,交錯執(zhí)行主隊列中任務(wù)和run loop中其他響應(yīng)事件援所。

    // 獲取主隊列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

3.2 獲取全局隊列dispatch_get_global_queue

全局隊列是并發(fā)隊列。

系統(tǒng)為每個應(yīng)用程序提供了四個不同優(yōu)先級的全局隊列欣除。因為這些隊列是全局的住拭,可以直接使用dispatch_get_global_queue函數(shù)請求隊列,不需要顯式創(chuàng)建历帚。

    // 獲取優(yōu)先級為QOS_CLASS_USER_INITIATED的全局隊列
    dispatch_queue_t aQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);

dispatch_get_global_queue函數(shù)的第一個參數(shù)指定Quality of Service(簡稱 QoS)滔岳,第二個參數(shù)是為未來擴展保留的。目前挽牢,總是設(shè)置為0谱煤。

指定優(yōu)先級的四個參數(shù)如下:

QoS Class 用途 任務(wù)所需時間
QOS_CLASS_USER_INTERACTIVE 優(yōu)先級與主線程任務(wù)相同,用于處理用戶正在等待禽拔、需要立即反饋的任務(wù)刘离。追求性能和響應(yīng)速度。 接近瞬間完成睹栖。
QOS_CLASS_USER_INITIATED 用于用戶發(fā)起的硫惕,需要立即獲得結(jié)果的任務(wù)。例如野来,打開磁盤上的文檔恼除,用戶點擊界面時執(zhí)行相應(yīng)操作,即用戶交互的下一步需要這一步的執(zhí)行結(jié)果梁只。對響應(yīng)和性能有較高要求的缚柳。 幾乎瞬間完成埃脏,如幾秒鐘或更少。
QOS_CLASS_UTILITY 需要一些時間來完成秋忙,不需要立即返回結(jié)果彩掐。例如,下載或?qū)霐?shù)據(jù)灰追。一般有提示進度的進度條堵幽。追求響應(yīng)和能源效率的平衡。 幾秒鐘至幾分鐘弹澎。
QOS_CLASS_BACKGROUND 在后臺運行朴下,不需要用戶看到。例如:索引苦蒿、同步殴胧、備份。關(guān)注能源效率佩迟。 幾分鐘到幾個小時团滥。

QOS_CLASS_USER_INTERACTIVE的優(yōu)先級與主線程相同,但QOS_CLASS_USER_INTERACTIVE仍然是在全局隊列报强,更新UI只能在主線程中灸姊。

沒有用戶交互時,app中任務(wù)應(yīng)當至少90%時間在utility及以下級別運行秉溉。

3.3 自定義隊列

除了使用系統(tǒng)提供的隊列力惯,還可以手動創(chuàng)建隊列。

    // 創(chuàng)建串行隊列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.serialQueue", DISPATCH_QUEUE_SERIAL);
    
    // 創(chuàng)建并發(fā)隊列
    dispatch_queue_t conQueue = dispatch_queue_create("com.GCD.conQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create函數(shù)有兩個參數(shù)召嘶,第一個參數(shù)指定隊列名稱父晶,debugger和性能工具會顯示此處隊列名稱以幫助跟蹤任務(wù)執(zhí)行情況。第二個參數(shù)指定是串行還是并發(fā)隊列苍蔬。在iOS 4.3之前诱建,該參數(shù)默認為NULL,隊列為串行隊列碟绑。

你可以創(chuàng)建任意數(shù)量串行隊列,但這些串行隊列之間是并發(fā)關(guān)系茎匠。例如格仲,創(chuàng)建了四個串行隊列,每個串行隊列執(zhí)行一個任務(wù)诵冒,系統(tǒng)可能同時執(zhí)行這四個任務(wù)凯肋。

雖然你可以創(chuàng)建任意數(shù)量的串行隊列,但不要寄希望于串行隊列間并發(fā)運行這一特性來提高性能汽馋,而應(yīng)該提交多個任務(wù)到并發(fā)隊列侮东。

4. 通過demo學習GCD

由于這篇文章的目的是優(yōu)化代碼圈盔,以及從不同線程安全地調(diào)用代碼。因此悄雅,你將通過下面網(wǎng)址下載一個模版文件驱敲。

Demo名稱:GrandCentralDispatch模板
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/GrandCentralDispatch模板

該模板文件是一個未優(yōu)化、非線程安全的demo宽闲。在該demo內(nèi)众眨,可以通過相冊或設(shè)定的網(wǎng)址獲取圖片,使用Core Image的CIDetectorAPI為圖片中的眼睛添加標記容诬。

運行下載的模版娩梨,如下所示:

GCDPreview.gif

可以看到當通過網(wǎng)絡(luò)獲取圖片時,UIAlertController過早的彈出览徒,稍后會修復(fù)這一問題狈定。

這個demo主要有以下四部分:

  • CollectionViewController:啟動程序后的第一個視圖控制器,通過略縮圖顯示所有照片习蓬。
  • DetailViewController:為眼睛添加圖像纽什,并在UIImageView中顯示圖片。
  • Photo:為來自NSURL類或ALAsset類的對象實例化照片友雳,最后提供照片稿湿、略縮圖。如果是通過網(wǎng)絡(luò)下載的照片押赊,還會提供下載狀態(tài)饺藤。
  • PhotoManager:管理Photo實例。

5. 使用dispatch_async處理后臺任務(wù)

再次打開app流礁,從相冊添加照片涕俗,或從網(wǎng)絡(luò)下載照片。

點擊CollectionViewController中的略縮圖神帅,觀察從略縮圖跳轉(zhuǎn)到DetailViewController的過程再姑,可以觀察到有明顯滯后。這是因為viewDidLoad中添加了過多的任務(wù)找御,導(dǎo)致初始化DetailViewController時間變長元镀。如果可能,最好減少viewDidLoad中工作量霎桅,加快視圖控制器加載速度栖疑。dispatch_async適合用來處理這一任務(wù)。

進入DetailViewController.m文件滔驶,使用下面代碼替換viewDidLoad中代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.imageView.image = self.image;
    NSAssert(self.image, @"Image not set; required to use view controller");
    
    // 1.將代碼從主線程移入全局隊列遇革,因為是異步執(zhí)行,不會堵塞主線程。
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        UIImage *overlayImage = [self faceOverlayImageFromImage:self.image];
        
        // 2.目前已經(jīng)獲取到新的照片萝快,在主線程更新UI锻霎。
        dispatch_async(dispatch_get_main_queue(), ^{
            [self fadeInNewImage:overlayImage];
        });
    });
}

再次運行demo,選擇略縮圖揪漩,可以看到DetailViewController初始化速度變快了旋恼,短暫延遲之后為眼睛添加圖像。如果加載一張更大照片氢拥,app也不會卡在加載視圖控制器環(huán)節(jié)了蚌铜。

dispatch_async提交任務(wù)到隊列后立即返回,由GCD決定任務(wù)具體執(zhí)行時間嫩海。當需要獲取網(wǎng)絡(luò)資源或進行大量計算時冬殃,應(yīng)當使用dispatch_async避免堵塞主線程。

dispatch_async常用隊列:

  • 自定義串行隊列Custom Serial Queue:和dispatch_async結(jié)合是很好的方案叁怪。
  • 主隊列Main Queue(Serial):完成并發(fā)隊列任務(wù)后常用主隊列更新UI审葬,這時的代碼通常在之前的并發(fā)隊列塊內(nèi)。如果當前在主隊列中奕谭,又以主隊列為目標調(diào)用dispatch_async涣觉,則會在當前方法結(jié)束后才執(zhí)行此任務(wù)。
  • 并發(fā)隊列Concurrent Queue:在后臺執(zhí)行非UI工作的常用選擇血柳。

6. 使用dispatch_after推遲任務(wù)

現(xiàn)在考慮下app的UE官册,當用戶第一次打開應(yīng)用時會疑惑這個應(yīng)用是做什么的?當PhotoManager類沒有任何photo實例時难捌,應(yīng)該添加一個提示幫助用戶了解如何使用這個app膝宁。同時需要注意的是,如果提示彈出太快根吁,用戶注意力可能在其他位置员淫,將不能捕捉到提示。延遲一秒顯示提示足以吸引到用戶的注意力击敌。

進入CollectionViewController.m文件介返,更新showOrHideNavPrompt方法颤专,如下所示:

- (void)showOrHideNavPrompt {
    NSUInteger count = [[PhotoManager sharedManager] photos].count;
    double delayInSeconds = 1.0;
    // 聲明需要延遲的時間冻辩,單位為納秒nanosecond,一秒的十億分之一痕届。
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    
    // 等待popTime時長后衡瓶,異步提交block到主隊列捅彻。
    dispatch_after(popTime, dispatch_get_main_queue(), ^{
        if (count) {
            self.headerSize = CGSizeZero;
            [self.collectionView reloadData];
        }
        else
        {
            self.headerSize = CGSizeMake(40, 40);
            [self.collectionView reloadData];
        }
    });
}

運行app,稍有延遲后鞍陨,sectionHeader部分會出現(xiàn)如何操作的提示。

dispatch_after的時間參數(shù)支持設(shè)置為DISPATCH_TIME_NOW,但不如直接使用dispatch_async诚撵;時間參數(shù)不支持設(shè)置為DISPATCH_TIME_FOREVER缭裆。

dispatch_after常用隊列:

  • 自定義串行隊列:需要小心使用,最好用在主隊列中寿烟。
  • 主隊列:這是dispatch_after的最佳隊列澈驼。Xcode有一個自動完成模版用來在主隊列使用dispatch_after
  • 并發(fā)隊列:很少需要在并發(fā)隊列使用dispatch_after筛武。

7. dispatch_once讓單例線程安全

單例(Singleton)常被同時從多個控制器調(diào)用缝其,因此,必須確保單例線程安全徘六。

單例模式的線程問題包括初始化内边、讀取、寫入等待锈。PhotoManager類被設(shè)計為單例漠其,但其目前并不是線程安全的「鸵簦可以通過創(chuàng)建race condition來驗證該問題和屎。

打開PhotoManager.m文件,sharedManager方法如下所示:

+ (instancetype)sharedManager {
    // 創(chuàng)建單例
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        // 初始化私有photosArray數(shù)組
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}

由于if語句不是線程安全的春瞬,多次調(diào)用會出現(xiàn)以下情況:一個線程(稱為線程A)進入if語句柴信,在sharedManager分配內(nèi)存前進行了context switch,另一線程(稱為線程B)進入了if語句宽气,初始化了一個sharedManager并結(jié)束随常。當系統(tǒng)再次context switch回線程A時,會再次初始化一個sharedManager抹竹。目前為止线罕,我們有兩個單例對象,而這不是我們想要的窃判。

為強制出現(xiàn)上面的情況钞楼,更新sharedManager方法代碼如下:

+ (instancetype)sharedManager {
    // 創(chuàng)建單例
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        // 通過使用sleepForTimeInterval:方法強制進行環(huán)境切換。
        [NSThread sleepForTimeInterval:2.0];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"%i %s",__LINE__, __PRETTY_FUNCTION__);
        NSLog(@"Singleton has memory address at: %@",sharedPhotoManager);
        [NSThread sleepForTimeInterval:2.0];
        // 初始化私有photosArray數(shù)組
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}

進入AppDelegate.m文件袄琳,導(dǎo)入PhotoManager.h文件询件,更新application:didFinishLaunchingWithOptions:方法如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
    
    // 創(chuàng)建兩個異步并發(fā)調(diào)用,來實例化單例唆樊,以產(chǎn)生race condition宛琅。通過輸出可以看出初始化了幾個單例對象。
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        [PhotoManager sharedManager];
    });
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        [PhotoManager sharedManager];
    });
    return YES;
}

運行app逗旁,控制臺輸出如下:

GrandCentralDispatch[1349:412491] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412490] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412490] Singleton has memory address at: <PhotoManager: 0x1c0010f60>
GrandCentralDispatch[1349:412491] Singleton has memory address at: <PhotoManager: 0x1c0010f60>
GrandCentralDispatch[1349:412376] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412376] Singleton has memory address at: <PhotoManager: 0x1c40114f0>

可以看到代碼的critical condition(即初始化單例部分)執(zhí)行了不止一次嘿辟,雖然這里是強制產(chǎn)生race condition舆瘪,但這種情況也會無意中發(fā)生。

線程問題可能難以調(diào)試红伦,因為它們往往難以重現(xiàn)英古。

為解決該錯誤,實例化代碼應(yīng)只執(zhí)行一次昙读,并在初始化的過程中阻止其他線程進入critical section召调,這正是dispatch_once所做的。

更新sharedManager方法如下:

+ (instancetype)sharedManager {
    // 創(chuàng)建單例
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 通過使用sleepForTimeInterval:方法強制進行環(huán)境切換蛮浑。
        [NSThread sleepForTimeInterval:2.0];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"%i %s",__LINE__, __PRETTY_FUNCTION__);
        NSLog(@"Singleton has memory address at: %@",sharedPhotoManager);
        [NSThread sleepForTimeInterval:2.0];
        // 初始化私有photosArray數(shù)組
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

運行demo唠叛,通過控制臺可以看到單例只有一個實例。

最后沮稚,移除AppDelegate.m中的dispatch_async部分代碼艺沼,移除sharedManager方法內(nèi)強制產(chǎn)生race condition的代碼和NSLog 輸出。

dispatch_once只是使共享的實例(即sharedManager)線程安全壮虫,而不會使類(即PhotoManager)線程安全澳厢,類中可能還會有其他critical section,如對數(shù)據(jù)操作部分囚似。這些critical section也需要使用其他方式實現(xiàn)線程安全剩拢,比如使用synchronize方式訪問數(shù)據(jù)。

8. 單例讀寫線程安全

在處理單例時饶唤,如果單例中的屬性是可變對象徐伐,那么需要考慮該對象本身是否線程安全。如果該屬性是Foundation中容器類募狂,則答案是不一定線程安全办素。一般不可變對象是線程安全,可變對象是非線程安全祸穷,這里的非線程安全很多時候指可以通過多個線程使用性穿,但不能同時使用,具體是否線程請查閱Apple文檔雷滚。

PhotoManager單例中使用的NSMutableArray是非線程安全的需曾。當多個線程同時讀取NSMutableArray時不會出現(xiàn)問題,但當有線程正在寫時祈远,其他線程進行讀取就會出現(xiàn)問題呆万。目前,PhotoManager單例并不能阻止這種情況發(fā)生车份。

現(xiàn)在查看PhotoManager.m文件中的addPhoto:photos方法:

// 這是一個寫方法谋减,其修改了私有可變數(shù)組對象。
- (void)addPhoto:(Photo *)photo {
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}

// 這是一個讀方法扫沼,通過獲取不可變副本防止調(diào)用者改變數(shù)據(jù)出爹。
- (NSArray *)photos {
    return [NSArray arrayWithObject:self.photosArray];
}

在讀方法中庄吼,通過獲取不可變副本可以防止調(diào)用者改變數(shù)據(jù),但不能阻止一個線程調(diào)用addPhoto:以政,同時另一線程調(diào)用photos霸褒。

這是軟件開發(fā)中經(jīng)典的Readers-Writes問題,GCD提供了一個優(yōu)雅的解決方案盈蛮,那就是使用dispatch barriers創(chuàng)建Readers-write lock

當dispatch barriers在concurrent queues隊列上使用時技矮,其充當了串行風格的隊列抖誉。使用GCD的barrier API可以確保提交到指定隊列的塊運行時,該隊列上其他任務(wù)不會運行衰倦。這意味著袒炉,執(zhí)行到dispatch barrier時,dispatch barrier的任務(wù)不會立即執(zhí)行樊零,會等待其他任務(wù)執(zhí)行完畢后才開始執(zhí)行我磁。

當dispatch barrier開始執(zhí)行任務(wù)時,隊列不會執(zhí)行其他任務(wù)驻襟。dispatch barrier任務(wù)執(zhí)行完畢后夺艰,隊列立即恢復(fù)至原來狀態(tài)。GCD提供了同步屏障dispatch_barrier_sync和異步屏障dispatch_barrier_async兩種方法沉衣。

下圖說明了barrier函數(shù)對異步塊的影響:

Dispatch Barrier

從上圖可以看到郁副,在進入dispatch barrier前,隊列就像正常的并發(fā)隊列豌习。當進入dispatch barrier隊列后存谎,隊列變?yōu)榱舜嘘犃校琤arrier block是唯一在執(zhí)行的任務(wù)肥隆。當barrier block結(jié)束后既荚,隊列立即恢復(fù)為原來的并發(fā)隊列。

創(chuàng)建一個自定義并發(fā)隊列栋艳,使用barrier函數(shù)區(qū)分開PhotoManager中讀寫操作恰聘,在自定義并發(fā)隊列中將允許多個線程同時讀取數(shù)據(jù)。

進入PhotoManager.m文件嘱巾,在私有接口部分聲明一個并發(fā)隊列憨琳,并更新addPhoto:方法:

@interface PhotoManager ()

@property (strong, nonatomic) NSMutableArray *photosArray;
@property (strong, nonatomic) dispatch_queue_t concurrentPhotoQueue;    // 添加此屬性。

@end

- (void)addPhoto:(Photo *)photo {
    if (photo) {
        // 將寫操作添加到barrier函數(shù)旬昭,以便執(zhí)行到寫操作時隊列只執(zhí)行這一項任務(wù)篙螟。
        dispatch_barrier_async(self.concurrentPhotoQueue, ^{
            // 由于dispatch_barrier_async函數(shù)的存在,執(zhí)行到下面操作時问拘,concurrentPhotoQueue隊列不會執(zhí)行其他任務(wù)遍略。
            [_photosArray addObject:photo];
            
            // 因為要更新UI惧所,所以在主隊列發(fā)送通知。
            dispatch_async(dispatch_get_main_queue(), ^{
                [self postContentAddedNotification];
            });
        });
    }
}

另外绪杏,還需要將讀操作添加到concurrentPhotoQueue隊列下愈。當讀操作執(zhí)行完畢后,寫操作才會開始蕾久,所以這里的讀操作沒有必要異步提交势似,dispatch_sync適用目前的情況。

使用dispatch_sync和dispatch barrier可以用來跟蹤任務(wù)進度僧著,或者用于等待操作完成才能獲取數(shù)據(jù)的情況履因。如果是第二種情況,需要在dispatch_sync塊外用__block修飾變量盹愚,點擊這里查看詳細介紹栅迄。

dispatch_sync常用隊列:

  • 自定義串行隊列:需要謹慎使用。如果dispatch_sync函數(shù)將任務(wù)提交至當前隊列皆怕,將產(chǎn)生死鎖毅舆。
  • 主隊列:與自定義串行隊列一樣,需要謹慎使用愈腾。
  • 并發(fā)隊列:非常好的選擇憋活。通過dispatch barrier同步任務(wù),或等待任務(wù)完成以便進一步處理數(shù)據(jù)顶滩。

繼續(xù)在PhotoManager.m文件內(nèi)余掖,更新photos方法如下:

- (NSArray *)photos {
    //  使用__block修飾,以便塊可以修改array內(nèi)容礁鲁。
    __block NSArray *array;
    
    // 同步執(zhí)行讀操作盐欺。
    dispatch_sync(self.concurrentPhotoQueue, ^{
        // 將讀操作結(jié)果保存到array,以便返回給調(diào)用者仅醇。
        array = self.photosArray;
    });
    return array;
}

最后冗美,初始化concurrentPhotoQueue屬性,更新sharedManager方法如下:

+ (instancetype)sharedManager {
    ...
    dispatch_once(&onceToken, ^{
        ...
        // 初始化concurrentPhotoQueue析二。
        sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.GCD.photoQueue", DISPATCH_QUEUE_CONCURRENT);
    });
    return sharedPhotoManager;
}

現(xiàn)在粉洼,PhotoManager已經(jīng)線程安全了,可以從任意線程讀取叶摄、寫入數(shù)據(jù)属韧。

Dispatch barrier常用隊列:

  • 自定義串行隊列:因為串行隊列每次執(zhí)行一個任務(wù),所以沒有必要在串行隊列使用barrier函數(shù)蛤吓。
  • 全局并發(fā)隊列:因為其他部分可能也在使用global concurrent queue宵喂,而執(zhí)行barrier函數(shù)部分時可能會堵塞線程,所以不要在全局并發(fā)隊列使用barrier函數(shù)会傲。
  • 自定義全局隊列:實現(xiàn)barrier函數(shù)的最佳隊列锅棕。

9. 使用dispatch group修復(fù)彈窗過早問題

當使用Internet獲取圖片時拙泽,UIAlertController會在圖片下載完成之前就彈出,如下所示:

dispatch group

錯誤出現(xiàn)在PhotoManager.mdownloadPhotoWithCompletionBlock:方法中:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
    __block NSError *error;
    for (int i=0; i<3; ++i) {
        NSURL *url;
        switch (i) {
            case 0:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 1:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 2:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            default:
                break;
        }
        
        Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
            if (_error) {
                error = _error;
            }
        }];
        
        [[PhotoManager sharedManager] addPhoto:photo];
    }
    
    if (completionBlock) {
        completionBlock(error);
    }
}

在上面方法的最后調(diào)用了completionBlock塊裸燎,這時我們在假設(shè)下載已經(jīng)完成顾瞻。但Photo類初始化方法下載圖片時使用的NSURLSession類是異步的,即調(diào)用下載后立即返回德绿,導(dǎo)致出現(xiàn)上面錯誤荷荤。

downloadPhotosWithCompletionBlock:方法應(yīng)該在所有圖片下載完畢后才調(diào)用自身的completionBlock,但是如何獲取異步并發(fā)任務(wù)的下載進度呢脆炎?我們無法獲知異步并發(fā)任務(wù)什么時間完成梅猿,也不知道其完成的順序。

也許秒裕,你可以寫很多BOOL屬性來跟蹤每一個下載任務(wù),但那樣會產(chǎn)生很多代碼钞啸,也很容易出錯几蜻。GCD中的dispatch group剛好可以解決這個問題。

在dispatch group內(nèi)任務(wù)完成時体斩,GCD API提供了兩種通知方式:

  • dispatch_group_wait:該函數(shù)會等待與dispatch group關(guān)聯(lián)的塊完成梭稚。如果在指定時間完成,返回0絮吵,dispatch group變?yōu)榭盏幕】荆蝗绻瑫r沒有完成,返回非0蹬敲,dispatch group恢復(fù)初始狀態(tài)暇昂;如果dispatch group沒有關(guān)聯(lián)任務(wù),則該函數(shù)立即返回伴嗡。
  • dispatch_group_notify:當dispatch group關(guān)聯(lián)塊完成時急波,該函數(shù)會將任務(wù)提交到指定隊列。如果dispatch group沒有關(guān)聯(lián)任務(wù)瘪校,則立即提交任務(wù)到指定隊列澄暮。提交任務(wù)后,group變?yōu)榭盏内逖铮梢酝ㄟ^dispatch_release釋放泣懊,或重用于其他塊對象。

9.1 dispatch_group_wait的使用

進入PhotoManager.m文件麻惶,更新downloadPhotosWithCompletionBlock:方法如下:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
    // 因為是在主線程中執(zhí)行dispatch_group_wait馍刮,使用dispatch_async將整個代碼添加到其他隊列避免堵塞主線程。
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        // 創(chuàng)建dispatch group,其更像是未完成任務(wù)的計數(shù)器用踩。
        dispatch_group_t downloadGroup = dispatch_group_create();
        
        __block NSError *error;
        for (int i=0; i<3; ++i) {
            NSURL *url;
            switch (i) {
                case 0:{
                    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
                    url = [NSURL URLWithString:urlString];
                    break;
                }
                    
                case 1:{
                    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
                    url = [NSURL URLWithString:urlString];
                    break;
                }
                    
                case 2:{
                    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
                    url = [NSURL URLWithString:urlString];
                    break;
                }
                    
                default:
                    break;
            }
            
            // 增加dispatch group中未完成任務(wù)數(shù)渠退,必須與dispatch_group_leave成對使用忙迁。
            dispatch_group_enter(downloadGroup);
            Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
                if (_error) {
                    error = _error;
                }
                
                // 減小dispatch group中未完成任務(wù)數(shù),必須與dispatch_group_enter成對使用碎乃。
                dispatch_group_leave(downloadGroup);
            }];
            
            [[PhotoManager sharedManager] addPhoto:photo];
        }
        
        // 等待downloadGroup內(nèi)任務(wù)完成姊扔。
        dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);
        
        // 目前,所有圖片已下載完成梅誓。在主隊列中調(diào)用完成處理程序恰梢。
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completionBlock) {
                completionBlock(error);
            }
        });
    });
}

一項任務(wù)可以添加到多個dispatch group,

運行demo梗掰,通過網(wǎng)絡(luò)獲取圖片嵌言,可以看到圖片下載完成后才出現(xiàn)的彈窗。

如果網(wǎng)絡(luò)速度太快及穗,不能清晰看到圖片下載進度和彈窗順序摧茴,可以打開設(shè)置 > 開發(fā)者 > Network Link Conditioner,選取其中的Very Bad Network開啟網(wǎng)速限制埂陆,使用完畢后記得關(guān)閉網(wǎng)速限制苛白。

9.2 dispatch_group_notify的使用

使用dispatch_group_wait會堵塞當前線程,dispatch_group_notify可以避免這種情況焚虱。

進入PhotoManager.m文件购裙,更新downloadPhotosWithCompletionBlock:方法如下:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
    // 因為dispatch_group_notify不會堵塞主線程,這里不需要使用dispatch_async鹃栽。
    dispatch_group_t downloadGroup = dispatch_group_create();
    
    __block NSError *error;
    for (int i=0; i<3; ++i) {
        NSURL *url;
        switch (i) {
            case 0:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 1:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 2:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            default:
                break;
        }
        
        dispatch_group_enter(downloadGroup);
        Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
            if (_error) {
                error = _error;
            }
            
            dispatch_group_leave(downloadGroup);
        }];
        
        [[PhotoManager sharedManager] addPhoto:photo];
    }
    
    // 當downloadGroup內(nèi)任務(wù)完成時躏率,將completionBlock塊提交到主隊列。
    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
        if (completionBlock) {
            completionBlock(error);
        }
    });
}

downloadGroup沒有關(guān)聯(lián)任務(wù)時民鼓,dispatch_group_notify立即將task提交到指定隊列薇芝。

除手動添加任務(wù)到dispatch group外,還可以使用dispatch_group_async提交任務(wù)到隊列的同時將任務(wù)添加到group摹察。

- (void)doSomething {
    dispatch_group_t testGroup = dispatch_group_create();
    
    dispatch_group_async(testGroup, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        // Do some work here.
    });
    
    dispatch_group_notify(testGroup, dispatch_get_main_queue(), ^{
        // Won't get here untill everything has finished.
    });
}

dispatch_group_async并不適合我們上面遇到的情況恩掷。因為圖片下載完成前就會返回,會導(dǎo)致dispatch group誤認為已經(jīng)完成任務(wù)供嚎,因此黄娘,必須在圖片下載的完成處理程序中手動調(diào)用dispatch_group_leave

dispatch group常用隊列:

  • 自定義串行隊列:當一組任務(wù)完成后克滴,使用dispatch group是很好選擇逼争。
  • 主隊列:可以很好使用。如果不想堵塞主線程劝赔,應(yīng)謹慎使用dispatch_group_wait誓焦。當多個長時間運行任務(wù)完成后(如網(wǎng)絡(luò)調(diào)用),異步模型更新UI是有吸引力的方式。
  • 并發(fā)隊列:這也是完成通知的理想選擇杂伟。

10. 使用dispatch_apply枚舉

繼續(xù)查看downloadPhotosWithCompletionBlock:方法移层,可以看到該方法內(nèi)有一個for循環(huán),for循環(huán)每次只能枚舉一個對象赫粥,依次枚舉观话。使用dispatch_apply可以并發(fā)枚舉,提高效率越平。

dispatch_apply函數(shù)提交block到指定隊列以進行多個調(diào)用频蛔,并在所有塊完成后返回。如果指定隊列是由dispatch_get_global_queue返回的并發(fā)隊列秦叛,則該塊可以并發(fā)調(diào)用晦溪。在并發(fā)隊列使用該函數(shù)時可以將其作為高效的for循環(huán),但dispatch_apply內(nèi)塊完成順序是無法確定的挣跋。

更新downloadPhotosWithCompletionBlock:方法如下:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
    dispatch_group_t downloadGroup = dispatch_group_create();
    __block NSError *error;
    
    // 使用dispatch_apply并發(fā)枚舉三圆。
    dispatch_apply(3, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i) {
        NSURL *url;
        switch (i) {
            case 0:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 1:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            case 2:{
                NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
                url = [NSURL URLWithString:urlString];
                break;
            }
                
            default:
                break;
        }
        
        dispatch_group_enter(downloadGroup);
        Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
            if (_error) {
                error = _error;
            }
            
            dispatch_group_leave(downloadGroup);
        }];
        
        [[PhotoManager sharedManager] addPhoto:photo];
    });
    
    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
        if (completionBlock) {
            completionBlock(error);
        }
    });
}

雖然PhotoManager單例線程安全,但下載圖片完成的進度可能不一致避咆,最終圖片順序不同嫌术。

運行demo,查看通過網(wǎng)絡(luò)獲取圖片是否更快了呢牌借?

在這個demo中,真的有必要使用dispatch_apply嗎割按?

  • dispatch_apply應(yīng)當用來迭代每一個循環(huán)都非常耗時的操作膨报。在這個demo中,可能創(chuàng)建線程的開銷大于并發(fā)帶來的性能提升适荣。
  • 不要過度優(yōu)化代碼现柠,應(yīng)當把有限的時間放在更值得的地方。使用Instruments查看性能瓶頸在哪里弛矛,優(yōu)化那些能顯著提高性能的地方够吩。
  • 通常,優(yōu)化代碼會使代碼更加復(fù)雜丈氓、難以維護周循,請確保增加的這些代碼是值得的。

dispatch_apply常用隊列:

  • 自定義串行隊列:在串行隊列中万俗,dispatch_apply無法并發(fā)枚舉湾笛,不如直接使用for循環(huán)。
  • 主隊列:和自定義串行隊列一樣闰歪,不是一個好的選擇嚎研。
  • 并發(fā)隊列:dispatch_apply在并發(fā)隊列可以并發(fā)枚舉,能夠提高效率库倘,還可以跟蹤任務(wù)進度临扮。

11. 使用dispatch semaphores

信號量(semaphore)是持有計數(shù)的信號论矾。

假設(shè)有一個房子(對應(yīng)進程概念),房子里住有多個人(對應(yīng)線程概念)杆勇,一個房子可以住多個人(一個進程可以包括多個線程)贪壳,這個房子(進程)的很多資源(如客廳、廚房)是所有人(線程)共享的靶橱。但有些地方(如臥室)最多只能有兩個人進去寥袭,這時可以在臥室門口掛上兩把鑰匙,進去的人拿著鑰匙進去关霸,出來時把鑰匙掛回門口传黄,沒有鑰匙不能進入臥室。

這時門口鑰匙數(shù)量就是信號量(semaphore)队寇。很明顯膘掰,信號量為0時需要等待,信號量大于0時佳遣,無需等待且減去一识埋。

進入GrandCentralDispatchTests.m文件,添加以下代碼:

// Xcode會在主線程測試所有以test開頭的方法零渐。
- (void)testPennyImageURL {
    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString, @"Penny.jpg"];
    [self downloadImageURLWithString:urlString];
}

- (void)testFriendsImageURL {
    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
    [self downloadImageURLWithString:urlString];
}

- (void)testNightKingImageURL {
    NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
    [self downloadImageURLWithString:urlString];
}

- (void)downloadImageURLWithString:(NSString *)urlString {
    // 創(chuàng)建信號窒舟,參數(shù)指信號量初始值。
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    NSURL *url = [NSURL URLWithString:urlString];
    __unused Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *error) {
        if (error) {
            XCTFail(@"%@ failed. %@",urlString, error);
        }

        // 增加信號量诵盼。
        dispatch_semaphore_signal(semaphore);
    }];
    
    dispatch_time_t timeOutTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC));
    // 等待信號10秒鐘(即timeOutTime)惠豺。這會堵塞線程,直到信號量大于0风宁。當超時沒有完成時洁墙,返回非零值,即測試失敗戒财。在指定時間完成時热监,返回零。
    if (dispatch_semaphore_wait(semaphore, timeOutTime)) {
        XCTFail(@"%@ timed out",urlString);
    }
}

點擊Product > Test測試(快捷鍵?+U)饮寞。斷開網(wǎng)絡(luò)再次測試孝扛,10秒中后會失敗。

Xcode中的測試是在XCTestCase子類上執(zhí)行的骂际,會運行任何以test開頭的方法疗琉。測試是在主線程進行的,所以多個測試是以串行方式進行的歉铝。

當測試返回后盈简,XCTest會認為該方法已完成,并進行下一個測試方法。這意味著下一個測試運行時柠贤,來自先前測試的異步代碼將繼續(xù)運行香浩。

通過網(wǎng)絡(luò)獲取代碼的方法通常是異步的,這使測試網(wǎng)絡(luò)相關(guān)代碼變得困難臼勉。這時我們可以像上面那樣使用dispatch_semaphore_wait阻止測試代碼完成邻吭。

總結(jié)

通過這篇文章,相信你對Grand Central Dispatch有一個整體的了解宴霸。以下是關(guān)于GCD的幾點總結(jié):

  • Dispatch queues之間是并發(fā)執(zhí)行任務(wù)的囱晴,串行僅限于串行隊列內(nèi)部。
  • GCD決定每刻同時執(zhí)行任務(wù)數(shù)量瓢谢。因此畸写,一個app中有100個隊列,每個隊列1個任務(wù)氓扛,但這100個任務(wù)并不會同時執(zhí)行枯芬,除非其有100個內(nèi)核。
  • 當要開始新任務(wù)時采郎,任務(wù)的優(yōu)先級Quality of Service會是一個參考要素千所。
  • Dispatch queues會復(fù)制添加到隊列的塊,并在完成時釋放該塊蒜埋。也就是說淫痰,不需要先顯式復(fù)制塊,再提交到隊列整份。因dispatch_sync同步執(zhí)行任務(wù)黑界,提交到dispatch_sync的塊不會執(zhí)行block_copy。
  • Dispatch queues自身是線程安全的皂林。也就是說,你可以將任務(wù)從任何線程提交到dispatch queue蚯撩,無需先鎖定或同步對隊列的訪問础倍。
  • 不要使用dispatch_sync將任務(wù)提交給當前隊列,那樣會產(chǎn)生死鎖胎挎。如果需要將任務(wù)添加到當前隊列沟启,請使用dispatch_async

盡管NSOperation和dispatch queues是執(zhí)行任務(wù)的首選方式犹菇,但你仍有可能需要創(chuàng)建自定義的線程德迹。例如:必須實時(real time)運行的代碼。GCD會盡可能快速運行任務(wù)揭芍,但其不是實時的胳搞。如果你確定需要自己創(chuàng)建線程,那么應(yīng)當創(chuàng)建盡可能少的線程。

Demo名稱:GrandCentralDispatch
源碼地址:https://github.com/pro648/BasicDemos-iOS

參考資料:

  1. Grand Central Dispatch In-Depth: Part 1/2
  2. iOS多線程編程總結(jié)
  3. Concurrency Programming Guide
  4. global_queue with qos_class_user_interactive
  5. Prioritize Work with Quality of Service Classes
  6. Using dispatch groups to wait for multiple web services

歡迎更多指正:https://github.com/pro648/tips/wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肌毅,一起剝皮案震驚了整個濱河市筷转,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悬而,老刑警劉巖呜舒,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異笨奠,居然都是意外死亡袭蝗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門般婆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來到腥,“玉大人,你說我怎么就攤上這事腺兴∽蟮纾” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵页响,是天一觀的道長篓足。 經(jīng)常有香客問我,道長闰蚕,這世上最難降的妖魔是什么栈拖? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮没陡,結(jié)果婚禮上涩哟,老公的妹妹穿的比我還像新娘。我一直安慰自己盼玄,他們只是感情好贴彼,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著埃儿,像睡著了一般器仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上童番,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天精钮,我揣著相機與錄音,去河邊找鬼剃斧。 笑死轨香,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的幼东。 我是一名探鬼主播臂容,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼科雳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了策橘?” 一聲冷哼從身側(cè)響起炸渡,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丽已,沒想到半個月后蚌堵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體音比,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡漱挎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年质涛,在試婚紗的時候發(fā)現(xiàn)自己被綠了米死。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陕凹。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡对碌,死狀恐怖栅哀,靈堂內(nèi)的尸體忽然破棺而出会通,到底是詐尸還是另有隱情丑婿,我是刑警寧澤性雄,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站羹奉,受9級特大地震影響秒旋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诀拭,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一迁筛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耕挨,春花似錦细卧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至翰苫,卻和暖如春插勤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背革骨。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留析恋,地道東北人良哲。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像助隧,于是被迫代替她去往敵國和親筑凫。 傳聞我的和親對象是個殘疾皇子滑沧,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345