本文由loveltyoic(博客)翻譯自raywenderlich,原文:Grand Central Dispatch Tutorial for Swift: Part 1/2
盡管Grand Central Dispatch(GCD)已經(jīng)存在一段時間了,但并非每個人都知道怎么使用它。這是情有可原的,因為并發(fā)很棘手,而且GCD本身基于C的API在Swift世界中很刺眼。 在這兩篇教程中,你會學到GCD的來龍去脈。第一部分解釋了GCD可以做什么和幾個基本功能。第二部分,你會學到一些GCD所提供的進階功能。
起步
libdispatch是Apple所提供的在IOS和OS X上進行并發(fā)編程的庫,而GCD正是它市場化的名字。GCD有如下優(yōu)點: – GCD可以將計算復雜的任務(wù)放到后臺執(zhí)行,從而提升app的響應(yīng)性能 – GCD提供了比鎖和線程更簡單的并發(fā)模型,幫助開發(fā)者避免并發(fā)的bug。
為了理解GCD,你需要了解一些線程和并發(fā)的概念。這些概念可能很含糊并且細微,所以先簡要回顧一下。
串行 vs 并發(fā)
這兩個詞用來描述任務(wù)的執(zhí)行順序。串行在同一時間點總是單獨執(zhí)行一個任務(wù),而并發(fā)可以同時執(zhí)行多個任務(wù)。
任務(wù)
在本教程中,你可以把任務(wù)當做一個閉包(closure)。實際上,你可以將GCD和函數(shù)指針一起使用,但是一般很少這樣使用。閉包更簡單!
不記得Swift中的閉包?閉包是自含的,可保存?zhèn)鬟f并被調(diào)用的代碼塊。當調(diào)用的時候,他們的用法很像函數(shù),可以有參數(shù)和返回值。除此之外,閉包可以“捕獲”外部的變量,也就是說,它可以看到并記住它自身被定義時的作用域變量。
Swift中的閉包和OC中的塊(block)類似甚至于他們幾乎就是可交換使用的。唯一的限制在于OC中不能使用Swift獨有的特性,比如元組(tuple)。但OC中的塊可以安全的替換成Swift中的閉包。
同步 vs 異步
這兩個詞描述的是函數(shù)何時將控制權(quán)返回給調(diào)用者,以及在返回時任務(wù)的完成情況。
同步函數(shù)只有在任務(wù)完成后才會返回。
異步函數(shù)會立即返回,不會等待任務(wù)完成。因此異步函數(shù)不會阻塞當前線程。
注意:當你讀到同步函數(shù)阻塞(block)當前進程或者函數(shù)是阻塞(blocking)函數(shù)時,不要困惑!動詞阻塞(block)描述的是函數(shù)對當前線程的影響,和塊(block)沒有關(guān)系。同時記住GCD文檔中有關(guān)OC的block可以跟Swift的閉包互換。
臨界區(qū)(Critical Section)
這是一段不能并發(fā)執(zhí)行的代碼,也就是說兩個線程不可以同時執(zhí)行它。這通常是因為這段代碼會修改共享的資源。否則,并發(fā)的進程同時修改同一個變量會導致錯誤。
競態(tài)條件
當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態(tài)條件。競態(tài)條件可能產(chǎn)生在代碼檢查時不易被發(fā)現(xiàn)的不可預期行為。
死鎖
兩個或更多的線程因等待彼此完成而陷入的困境稱為死鎖。第一個線程無法完成因為它在等待第二個線程完成。但是第二個線程也無法完成因為它在等待第一個線程完成。
線程安全
線程安全的代碼是可以被多個線程或并發(fā)任務(wù)安全調(diào)用的,他不會造成任何問題(數(shù)據(jù)錯誤,崩潰等)。非線程安全的代碼在同一時間只能單獨執(zhí)行。一段線程安全的代碼如let a = ["thread-safe"]。由于數(shù)組是只讀的,它可以被多個線程同時使用而不會引發(fā)問題。另一方面,var a = ["thread-unsafe"]是可變數(shù)組。這意味著它不是線程安全的,因為多個線程可以同時獲取并修改這個數(shù)組,會得到不可預料的結(jié)果。非線程安全的變量和可變的數(shù)據(jù)結(jié)構(gòu)在同一時刻應(yīng)該只能被一個線程獲取。
上下文切換
上下文切換是在進程中切換不同線程時保存和恢復程序執(zhí)行狀態(tài)的過程。這一過程在編寫多任務(wù)app時相當常見,但是會造成一些額外開支。
并發(fā) vs 并行
并發(fā)和并行經(jīng)常會被同時提起,所以值得通過簡短的解釋來區(qū)分彼此。
并發(fā)代碼中的單獨部分可以同時執(zhí)行。然而,這要由系統(tǒng)來決定并發(fā)怎樣發(fā)生或是否發(fā)生。
多核設(shè)備通過并行來同時執(zhí)行多個線程;然而,在單核設(shè)備中,必須要通過上下文切換來運行另一個線程或進程。這一過程通常發(fā)生的很快以至于給人并行的假象。如下圖所示:
盡管你可能在GCD之下編寫并發(fā)執(zhí)行的代碼,但仍由GCD來決定并行的需求有多大。
深層次的觀點是并發(fā)實際上是關(guān)乎結(jié)構(gòu)的。當你編寫GCD代碼時,你組織你的代碼來揭示出可以同時運行的工作,以及不可以同時運行的。如果你想深入了解這個主題,猛擊Rob Pike。
隊列
GCD提供了調(diào)度隊列(dispatch queues)來處理提交的任務(wù);這些隊列管理著你向GCD提交的任務(wù)并且以先進先出(FIFO)的順序來執(zhí)行任務(wù)。這保證了第一個加入隊列的任務(wù)第一個被執(zhí)行,第二個加入的任務(wù)第二個開始執(zhí)行,以此類推。
所有調(diào)度隊列都是線程安全的從而讓你可以同時在多個線程中使用它們。當你明白了調(diào)度隊列如何為你的代碼提供了線程安全性時,GCD的優(yōu)點就很明顯了。關(guān)鍵是選擇正確的調(diào)度隊列種類和正確的調(diào)度函數(shù)(dispatching function)來提交你的任務(wù)。
順序隊列
順序隊列中的任務(wù)同一時間只執(zhí)行一件任務(wù),每件任務(wù)只有在先前的任務(wù)完成后才開始。同時,你并不知道一個任務(wù)完成到另一個任務(wù)開始之間的間隔時間,如下圖所示:
任務(wù)的執(zhí)行是在GCD掌控之下的;你唯一確定的就是GCD在同一時刻只執(zhí)行一件任務(wù)并且按任務(wù)加入隊列的順序執(zhí)行。
因為不會在順序隊列中同時執(zhí)行兩件任務(wù),所以沒有多個任務(wù)同時進入臨界區(qū)的危險;這保證了臨界區(qū)不會出現(xiàn)競態(tài)條件。因此如果進入臨界區(qū)的唯一途徑就是通過向調(diào)度隊列提交任務(wù),那么可以保證臨界區(qū)是安全的。
并發(fā)隊列
并發(fā)隊列中的任務(wù)可以保證按進入隊列的順序被執(zhí)行…僅此而已!任務(wù)可能以任意順序完成而且你不知道何時下一個任務(wù)會開始,或是任一時刻有多少任務(wù)在運行。再一次,這完全取決于GCD。 下圖展示了四個并發(fā)任務(wù)的例子:
任務(wù)1,2和3都運行的很快,一個接一個。但是任務(wù)1在任務(wù)0開始了一段時間后才開始。同時,任務(wù)3在任務(wù)2開始后才開始但是卻更早完成。
何時開始一個任務(wù)完全取決于GCD。如果一個任務(wù)的執(zhí)行時間和另一個的發(fā)生重疊,將由GCD來決定是否要將任務(wù)運行在另一個可用的核上或是通過上下文切換來運行另一個程序。
有趣的是,GCD為每種隊列類型提供了至少5種特別的隊列。
隊列類型
首先,系統(tǒng)提供了一種特殊的順序隊列main queue。和其他的順序隊列一樣,在這個隊列里的任務(wù)同一時刻只有一個在執(zhí)行。然而,這個隊列保證了所有任務(wù)會在主線程中執(zhí)行,主線程是唯一一個允許更新UI的線程。這個隊列用來向UIView對象發(fā)消息或發(fā)通知。
系統(tǒng)同時提供了幾種并發(fā)隊列。這些隊列和它們自身的QoS等級相關(guān)。QoS等級表示了提交任務(wù)的意圖,使得GCD可以決定如何制定優(yōu)先級。
QOS_CLASS_USER_INTERACTIVE: user interactive等級表示任務(wù)需要被立即執(zhí)行以提供好的用戶體驗。使用它來更新UI,響應(yīng)事件以及需要低延時的小工作量任務(wù)。這個等級的工作總量應(yīng)該保持較小規(guī)模。
QOS_CLASS_USER_INITIATED:user initiated等級表示任務(wù)由UI發(fā)起并且可以異步執(zhí)行。它應(yīng)該用在用戶需要即時的結(jié)果同時又要求可以繼續(xù)交互的任務(wù)。
QOS_CLASS_UTILITY:utility等級表示需要長時間運行的任務(wù),常常伴隨有用戶可見的進度指示器。使用它來做計算,I/O,網(wǎng)絡(luò),持續(xù)的數(shù)據(jù)填充等任務(wù)。這個等級被設(shè)計成節(jié)能的。
QOS_CLASS_BACKGROUND:background等級表示那些用戶不會察覺的任務(wù)。使用它來執(zhí)行預加載,維護或是其它不需用戶交互和對時間不敏感的任務(wù)。
要清楚Apple的API同時也使用了全局調(diào)度隊列(global dispatch queue),所以你添加的任何任務(wù)都不是這些隊列中的唯一任務(wù)。
最后,你可以創(chuàng)建自定義的順序或并發(fā)隊列。意味著你至少有5種隊列:主隊列(main queue),四種通用調(diào)度隊列,加上任意你自己定制的隊列!
以上就是調(diào)度隊列的主要部分!
GCD的“藝術(shù)”可歸結(jié)為選擇正確的隊列調(diào)度函數(shù)來提交任務(wù)。最佳的學習方式就是通過下面的例子。
示例
因為這篇教程的目標是使用GCD優(yōu)化程序以及在不同線程中安全的運行代碼,所以你會以一個幾近完成的項目GooglyPuff來開始。
GooglyPuff是一個未優(yōu)化,非線程安全的app,使用Core Image的人臉識別API在人臉上疊加金魚眼。初始圖像可以從圖片庫中選擇或是從網(wǎng)絡(luò)下載一組預定的圖片。
一旦下載了工程,提取到合適的地方,打開Xcode并運行它??雌饋砣缦拢?nbsp;
注意:當你選擇Le Internet選項來下載圖片時,一個UIAlertController提示框會過早的彈出。你會在教程的第二部分修復這個問題。
這個工程中有4個需要關(guān)心的類: – PhotoCollectionViewController:app啟動后的第一個視圖控制器。展示所有選擇的圖片的縮略圖。 – PhotoDetailViewController:為圖片加上金魚眼并在UIScrollView中展示。 – Photo:描述圖片屬性的協(xié)議。提供圖片,縮略圖和狀態(tài)。兩個類實現(xiàn)了這個協(xié)議:DownloadPhoto從NSURL實例化圖片,AssetPhoto從ALAsset實例化圖片。 – PhotoManager:管理所有Photo對象。
使用dispatch_sync處理后臺任務(wù)
返回app并從圖片庫中添加一些圖片或使用Le Internet選項下載一些。
留意在輕觸PhotoCollectionViewController中的UICollectionViewCell后要多久才能完成PhotoDetailViewController的初始化;此時存在明顯的延遲,尤其是在較慢的設(shè)備上瀏覽較大的圖片時。
一不小心就會在UIViewController的viewDidLoad中填充過多雜亂的方法而造成超負荷;以至于經(jīng)常要等待很久視圖控制器才會出現(xiàn)。如果可能的話,最好將一些工作轉(zhuǎn)移到后臺去完成,如果這些工作在加載時不是必需的。
聽起來是使用dispatch_async的時候!
打開PhotoDetailViewController然后用下面的實現(xiàn)替換viewDidload:
override func viewDidLoad() { super.viewDidLoad() assert(image != nil, "Image not set; required to use view controller") photoImageView.image = image // Resize if neccessary to ensure it's not pixelated if image.size.height <= photoImageView.bounds.size.height && image.size.width <= photoImageView.bounds.size.width { photoImageView.contentMode = .Center } dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1 let overlayImage = self.faceOverlayImageFromImage(self.image) dispatch_async(dispatch_get_main_queue()) { // 2 self.fadeInNewImage(overlayImage) // 3 } } }
上面代碼的工作流程: 1. 首先將工作從主線程上轉(zhuǎn)移到全局隊列中。因為這是一個dispatch_async調(diào)用,異步提交的閉包意味著調(diào)用線程會繼續(xù)執(zhí)行下去。這使得viewDidLoad在主線程上更早的完成從而讓加載的過程在感覺上更迅速。同時,人臉識別過程已經(jīng)開始并會在晚些時候完成。 2. 在這時,人臉識別已經(jīng)完成并生成一張新圖片。因為要用這張新圖片更新UIImageView,所以把一個閉包加入主線程中。記住 — 必須總是在主線程中操作UIKit! 3. 最后,用fadeInNewImage更新UI。
注意:你在使用Swift的尾隨閉包(trailing closure)語法,將閉包寫在參數(shù)括號的后面?zhèn)鹘odispatch_async。這種語法看起來更清晰,因為閉包沒有內(nèi)嵌到函數(shù)括號中。
運行app;選擇一張圖片然后你會明顯地發(fā)現(xiàn)視圖控制器載入更快了,隨后金魚眼會加入進來。這給app帶來了很好的效果,因為你展示出圖片修改前后的變化。同時,如果你試圖加載一張極其巨大的圖片,app不會因為加載視圖控制器而失去響應(yīng),這讓app有很好的適應(yīng)性。
正如前面所提到的,dispatch_async以閉包的形式向隊列中追加了一項任務(wù)并立即返回了。這項任務(wù)會在GCD決定的稍后時間執(zhí)行。當你需要執(zhí)行網(wǎng)絡(luò)請求或在后臺執(zhí)行繁重的CPU任務(wù)時,使用dispatch_async不會阻塞當前進程。
何時使用何種隊列類型快速指南: – 自定義順序隊列:當你想順序執(zhí)行后臺任務(wù)并追蹤它時,這是一個很好的選擇。因為同時只有一個任務(wù)在執(zhí)行,因此消除了資源競爭。注意如果需要從方法中獲取數(shù)據(jù),你必須內(nèi)置另一個閉包來得到它或者考慮使用dispatch_sync。 – 主隊列(順序):當并發(fā)隊列中的任務(wù)完成需要更新UI的時候,這是一個通常的選擇。為達此目的,需要在一個閉包中嵌入另一個閉包。同時,如果在主隊列中調(diào)用dispatch_async來返回主隊列,能保證新的任務(wù)會在當前方法完成后再執(zhí)行。 – 并發(fā)隊列:通常用來執(zhí)行與UI無關(guān)的后臺任務(wù)。
獲取全局隊列的幫助變量(Helper Variable)
你可能注意到dispatch_get_global_queue的QoS等級參數(shù)寫起來有些繁瑣。這是由于qos_class_t被定義為一個結(jié)構(gòu)體,它包含有Uint32型的屬性value,而這個屬性需要被轉(zhuǎn)型為Int。在Utils.swift中添加一些全局的計算變量,使獲取全局隊列更方便一些:
var GlobalMainQueue: dispatch_queue_t { return dispatch_get_main_queue() } var GlobalUserInteractiveQueue: dispatch_queue_t { return dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.value), 0) } var GlobalUserInitiatedQueue: dispatch_queue_t { return dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0) } var GlobalUtilityQueue: dispatch_queue_t { return dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0) } var GlobalBackgroundQueue: dispatch_queue_t { return dispatch_get_global_queue(Int(QOS_CLASS_BACKGROUND.value), 0) }回到PhotoDetailViewController中的viewDidLoad中,將dispatch_get_global_queue和dispatch_get_main_queue替換為幫助變量:
dispatch_async(GlobalUserInitiatedQueue) { let overlayImage = self.faceOverlayImageFromImage(self.image) dispatch_async(GlobalMainQueue) { self.fadeInNewImage(overlayImage) } }
這使得調(diào)度調(diào)用更易讀并且很容易看出在使用哪個隊列。
用dispatch_after推遲任務(wù)
仔細思考你的app中的UX。用戶可能在第一次打開app的時候不知道該做什么,不是嗎?
如果在PhotoManager類中沒有圖片的時候,給用戶一個提示是個不錯的主意。然而,你同時要考慮用戶的視線怎樣掃過屏幕:如果提示出現(xiàn)的太快,用戶可能還在看其他的地方而忽略了提示。
推遲一秒鐘再出現(xiàn)提示,此時便可抓住用戶的注意力,因為他們已經(jīng)對app有了第一印象。
將下面的代碼加到showOrHideNavPrompt的實現(xiàn)中,它位于PhotoCollectionViewController.swift文件底部。
func showOrHideNavPrompt() { let delayInSeconds = 1.0 let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC))) // 1 dispatch_after(popTime, GlobalMainQueue) { // 2 let count = PhotoManager.sharedManager.photos.count if count > 0 { self.navigationItem.prompt = nil } else { self.navigationItem.prompt = "Add photos with faces to Googlyify them!" } } }
showOrHideNavPrompt會在viewDidLoad以及UICollectionView重新加載的時候被執(zhí)行。代碼解釋如下: 1. 聲明推遲的時間。 2. 等待delayInSeconds所表示的時間,然后將閉包異步地加入主隊列中。
運行app。在短暫的延遲后,提示會出現(xiàn)并吸引用戶的注意。
dispatch_after的工作原理就像推遲的dispatch_async。一旦dispatch_after返回,你還是無法掌握實際的執(zhí)行時間抑或是取消任務(wù)。
想知道何時使用dispatch_after?
自定義順序隊列:慎用。在自定義順序隊列中慎用dispatch_after。你最好留在主隊列中。
主隊列(順序):好主意。在主隊列中使用dispatch_after是一個好主意;Xcode對此有自動補全模板。
并發(fā)隊列:慎用。很少會這樣使用,最好留在主隊列中。
單例和線程安全
單例。愛也好,恨也罷,它們在iOS中就像貓之于互聯(lián)網(wǎng)一樣流行。
經(jīng)常有人因為單例不是線程安全的而憂慮。這種擔憂是很有道理的,考慮到他們的用法:單例經(jīng)常被多個控制器同時使用。PhotoManager類是一個單例,所以你要仔細思考這個問題。
思考兩種情形,初始化單例的過程和對他進行讀寫的過程。
先來看初始化。這看起來很簡單,因為Swift在全局域中初始化變量。在Swift中,全局變量在首次使用時被初始化,并且保證初始化是原子操作。也就是說,初始化代碼被視為臨界區(qū)從而保證了初始化在其他線程使用全局變量之前就完成了。Swift是怎么做到的?其實,Swift在幕后使用了GCD中的dispatch_once,詳見博客。
dispatch_once以線程安全的方式執(zhí)行且僅執(zhí)行一次閉包。如果一個線程正處于臨界區(qū)中 — 被提交給dispatch_once的任務(wù) — 其他線程會阻塞直到它完成。并且一旦它完成,其他線程不會再執(zhí)行臨界區(qū)中的代碼。用let將單例定義為全局常量,我們可以進一步保證變量在初始化后不會發(fā)生變化。從某種意義上說,所有Swift全局常亮量都天生是單例,并且線程安全地初始化。
但是我們?nèi)孕枰紤]讀和寫。盡管Swift使用dispatch_once來確保單例初始化是線程安全的,但不能保證它所表示的數(shù)據(jù)類型也是線程安全的。例如用一個全局變量來聲明一個類實例,但在類中還是會有修改類內(nèi)部數(shù)據(jù)的臨界區(qū)。此時就需要其他方式來達成線程安全,比如通過對數(shù)據(jù)的同步化使用(synchronizing access)。
處理讀寫問題
實例化線程安全性不是單例的唯一問題。如果單例的屬性表示一個可變對象,比如PhotoManager中的photos,那么你就需要考慮那個對象是否線程安全。
在Swift中任意用let聲明的常量都是只讀并且線程安全的。用var聲明的變量是可變且非線程安全的,除非數(shù)據(jù)類型本身被設(shè)計成線程安全。Swift中的集合類型比如Array和Dictionary,當聲明為變量時不是線程安全的。那么像Foundation的容器NSArray呢?是線程安全的嗎?答案是—“可能不是”!Apple維護的一個幫助列表中有許多Foundation中非線程安全的類。
盡管很多線程可以同時讀取一個Array的可變實例而不出問題,但如果一個線程在修改數(shù)組的同時另一個線程卻在讀取這個數(shù)組,這是不安全的。你的單例目前還不能阻止這種情況發(fā)生。
為了弄清楚問題,看看PhotoManager.swift中的addPhoto:
func addPhoto(photo: Photo) { _photos.append(photo) dispatch_async(dispatch_get_main_queue()) { self.postContentAddedNotification() } }
這是一個寫方法,因為它修改了一個可變數(shù)組。
再看看photos屬性:
private var _photos: [Photo] = [] var photos: [Photo] { return _photos }
這個屬性的getter方法是一個讀方法。調(diào)用者得到一個數(shù)組的拷貝并且保護了原始數(shù)組不被改變,但是這不能保證一個線程在調(diào)用addPhoto來寫的時候沒有另一個線程同時也在調(diào)用getter方法讀photos屬性。
注意:
在上面的代碼中,為什么調(diào)用者要獲取photo數(shù)組的拷貝?在Swift中,參數(shù)或函數(shù)返回是通過值或引用來傳遞的。引用傳遞和OC中的傳指針一樣,這意味著你得到的是原始的對象,對這個對象的修改會影響到其他使用了這個對象引用的代碼。值傳遞拷貝了對象本身,對拷貝的修改不會影響原始的對象。默認情況下,Swift類實例是引用傳遞而結(jié)構(gòu)體是值傳遞。
Swift內(nèi)置的數(shù)據(jù)類型,如Array和Dictionary,是用結(jié)構(gòu)體來實現(xiàn)的,看起來傳遞集合類型會造成代碼中出現(xiàn)大量的拷貝。不要因此擔心內(nèi)存使用問題。Swift的集合類型經(jīng)過優(yōu)化,只有在需要的時候才進行拷貝,比如通過值傳遞的數(shù)組在第一次被修改的時候。
這是軟件開發(fā)中經(jīng)典的讀者寫者問題(Readers-Writers Problem)。GCD使用調(diào)度屏障(dispatch barriers)提供了一個優(yōu)雅的解決方案來生成讀寫鎖。
當跟并發(fā)隊列一起工作時,調(diào)度屏障是一族行為像序列化瓶頸的函數(shù)。使用GCD的barrier API確保了提交的閉包是指定隊列中在特定時段唯一在執(zhí)行的一個。也就是說必須在所有先于調(diào)度屏障提交的任務(wù)已經(jīng)完成的情況下,閉包才能開始執(zhí)行。
當輪到閉包時,屏障執(zhí)行這個閉包并確保隊列在此過程不會執(zhí)行其他任務(wù)。一旦閉包完成,隊列返回到默認的執(zhí)行方式。GCD同時提供了同步和異步兩種屏障函數(shù)。
下圖說明了屏障函數(shù)應(yīng)用于多個異步任務(wù)的效果:
注意隊列開始就像普通的并發(fā)隊列一樣工作。但當屏障執(zhí)行的時候,隊列變成像順序隊列一樣。就是說,屏障是唯一一個在執(zhí)行的任務(wù)。在屏障完成后,隊列恢復成普通的并發(fā)隊列。
下面說明什么時候用 — 什么時候不應(yīng)該用 — 屏障函數(shù):
自定義順序隊列:壞選擇。因為順序隊列本身就是順序執(zhí)行,屏障不會起到任何幫助作用。
全局并發(fā)隊列:慎用。其他系統(tǒng)可能也在使用隊列,你不應(yīng)該出于自身目的而獨占隊列。
自定義并發(fā)隊列:最佳選擇。用于原子操作或是臨界區(qū)代碼。任何需要線程安全的設(shè)置和初始化都可以使用屏障。
因為以上唯一合適的選擇就是自定義并發(fā)隊列,你需要生成一個這樣的隊列來處理屏障函數(shù)以隔離讀寫操作。并發(fā)隊列允許多個線程同時的讀操作。
打開PhotoManager.swift并在photos屬性下面添加如下私有屬性到類中:
private let concurrentPhotoQueue = dispatch_queue_create( "com.raywenderlich.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)使用dispatch_queue_create初始化一個并發(fā)隊列concurrentPhotoQueue。第一個參數(shù)遵循反向DNS命名習慣;保證描述性以利于調(diào)試。第二個參數(shù)指出你的隊列是順序的還是并發(fā)的。
注意:當在網(wǎng)上搜索例子時,你經(jīng)??吹饺藗儌?或NULL作為dispatch_queue_create的第二個參數(shù)。這是一種過時的方法來生成順序調(diào)度隊列;最好用參數(shù)顯示聲明。 找到addPhoto并用如下實現(xiàn)替換之:
func addPhoto(photo: Photo) { dispatch_barrier_async(concurrentPhotoQueue) { // 1 self._photos.append(photo) // 2 dispatch_async(GlobalMainQueue) { // 3 self.postContentAddedNotification() } } }
來看這段代碼如何工作的: 1. 將寫操作加入自定義的隊列中。當臨界區(qū)被執(zhí)行時,這是隊列中唯一一個在執(zhí)行的任務(wù)。 2. 將對象加入數(shù)組。因為是屏障閉包,這個閉包不會和concurrentPhotoQueue中的其他任務(wù)同時執(zhí)行。 3. 最終發(fā)送一個添加了圖片的通知。這個通知應(yīng)該在主線程中發(fā)送因為這涉及到UI,所以這里分派另一個異步任務(wù)到主隊列中。
這個任務(wù)解決了寫問題,但是你還需要實現(xiàn)photos的讀方法。
為確保和寫操作保持線程安全,你需要在concurrentPhotoQueue中執(zhí)行讀操作。但是你需要從函數(shù)返回讀數(shù)據(jù),所以不能異步地提交讀操作到隊列里,因為異步任務(wù)不能保證在函數(shù)返回前執(zhí)行。
因此,dispatch_sync是個極好的候選。
dispatch_sync同步提交任務(wù)并等到任務(wù)完成后才返回。使用dispatch_sync和調(diào)度屏障一起來跟蹤任務(wù);或是在需要等待返回數(shù)據(jù)時使用dispatch_sync。
仍需小心。設(shè)想你調(diào)用dispatch_sync到當前隊列中。這會造成死鎖。因為調(diào)用在等待閉包完成,但是閉包無法完成(甚至根本沒開始?。?,直到當前在執(zhí)行的任務(wù)結(jié)束,但當前任務(wù)沒法結(jié)束(因為阻塞的閉包還沒完成)!這就要求你必須清醒的認識到你從哪個隊列調(diào)用了閉包,以及你將任務(wù)提交到哪個隊列。
概述一下何時何地使用dispatch_sync: – 自定義順序隊列:非常小心;如果你在運行一個隊列時調(diào)用dispatch_sync調(diào)度任務(wù)到同一個隊列,你顯然會制造死鎖。 – 主隊列(順序):非常小心,原理同上。 – 并發(fā)隊列:好選擇。用在和調(diào)度屏障同步或是等待任務(wù)完成以繼續(xù)后續(xù)處理。 還是在PhotoManager.swift中,替換photos如下:
var photos: [Photo] { var photosCopy: [Photo]! dispatch_sync(concurrentPhotoQueue) { // 1 photosCopy = self._photos // 2 } return photosCopy }
分別來看每個號碼注釋: 1. 同步調(diào)度到concurrentPhotoQueue隊列執(zhí)行讀操作。 2. 保存圖片數(shù)組的拷貝到photoCopy并返回它。
恭喜 —— 你的PhotoManager單例已經(jīng)是線程安全的了。不論你讀或是寫圖片數(shù)組,你都有信心保證操作會安全的執(zhí)行。
回顧
還是不能100%的確定GCD的本質(zhì)?你可以自己創(chuàng)建使用GCD函數(shù)的簡單例子,通過斷點和NSLog來確保你明白發(fā)生了什么。
我這里有兩張動態(tài)GIF圖片來幫助你理解dispatch_async和dispatch_sync。每張GIF上面都有代碼輔助你理解;注意代碼中的斷點和相應(yīng)的隊列狀態(tài)。
重訪dispatch_sync
override func viewDidLoad() { super.viewDidLoad() dispatch_sync(dispatch_get_global_queue( Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) { NSLog("First Log") } NSLog("Second Log") }
下面對圖片中的幾個狀態(tài)做說明:
1. 主隊列按部就班的執(zhí)行任務(wù) —— 緊接著的任務(wù)是實例化包含viewDidLoad的UIViewController類。
2. viewDidLoad在主線程中執(zhí)行。
3. dispatch_sync閉包被加入到全局隊列中稍后執(zhí)行。主線程停下來等待閉包完成。同時,全局隊列正在并發(fā)執(zhí)行任務(wù);記住閉包以FIFO的順序從全局隊列中取出,但是會并發(fā)地執(zhí)行。全局隊列首先處理dispatch_sync閉包加入前已經(jīng)存在隊列中的任務(wù)。
4. 最后,輪到dispatch_sync閉包執(zhí)行。
5. 閉包執(zhí)行完畢,主線程得以繼續(xù)。
6. viewDidLoad方法完成,主隊列接著處理其它任務(wù)。
dispatch_sync把任務(wù)加入隊列并一直等待其完成。dispatch_async做了差不多的工作,只是它不會等待任務(wù)完成,而是轉(zhuǎn)而去繼續(xù)其他工作。
重訪dispatch_async
override func viewDidLoad() { super.viewDidLoad() dispatch_async(dispatch_get_global_queue( Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) { NSLog("First Log") } NSLog("Second Log") }
1.主隊列按部就班的執(zhí)行任務(wù) —— 緊接著的任務(wù)是實例化包含viewDidLoad的UIViewController類。
2.viewDidLoad在主線程中執(zhí)行。
3.dispatch_async閉包被加入到全局隊列中稍后執(zhí)行。
4.viewDidLoad在dispatch_async后繼續(xù)向下執(zhí)行,主線程繼續(xù)其他任務(wù)。同時,全局隊列正在并發(fā)執(zhí)行任務(wù);記住閉包以FIFO的順序從全局隊列中取出,但是會并發(fā)地執(zhí)行。
5.執(zhí)行dispatch_async所添加的閉包。
6.dispatch_async閉包完成,NSLog輸出到控制臺。
在這個特別的例子中,第一個NSLog在第二個NSLog后執(zhí)行。事實并非總是如此——這取決于硬件在彼時正在做什么,你無法控制或知曉哪個語句會先執(zhí)行?!暗谝粋€”NSLog在某種調(diào)用情況下可能會先執(zhí)行。
下一步?
在本教程中,你已經(jīng)學到了如何編寫線程安全的代碼以及如何在保持主線程響應(yīng)性的前提下執(zhí)行CPU密集型的任務(wù)。
可以下載GooglyPuff,里面包含了本教程中所做的所有改進。教程的第二部分會在此基礎(chǔ)上繼續(xù)改進。
如果你打算優(yōu)化自己的app,你真的應(yīng)該使用Instruments中的Time Profile模板來測試。使用方法已經(jīng)超出本教程范圍,可以查看怎樣使用Instruments。
同時確保你在真機上測試,因為在模擬器上測試會得到跟真實體驗相差甚遠的結(jié)果。
在教程的下篇你會更深入GCD的API中做些更酷的事情。