swift 超級詳細(xì)的GCD講解(1)

盡管 Grand Central Dispatch (GCD)已經(jīng)存在一段時間了熊经,但并非每個人都知道怎么使用它磷蛹。這是情有可原的瞳别,因為并發(fā)很棘手轩触,而且GCD本身基于C的API在Swift世界中很刺眼。

在這兩篇教程中匠襟,你會學(xué)到GCD的來龍去脈钝侠。第一部分解釋了GCD可以做什么和幾個基本功能。第二部分宅此,你會學(xué)到一些GCD所提供的進(jìn)階功能。

more

起步

libdispatch是Apple所提供的在IOS和OS X上進(jìn)行并發(fā)編程的庫爬范,而GCD正是它市場化的名字父腕。GCD有如下優(yōu)點:

- GCD可以將計算復(fù)雜的任務(wù)放到后臺執(zhí)行,從而提升app的響應(yīng)性能

- GCD提供了比鎖和線程更簡單的并發(fā)模型青瀑,幫助開發(fā)者避免并發(fā)的bug璧亮。

為了理解GCD,你需要了解一些線程和并發(fā)的概念斥难。這些概念可能很含糊并且細(xì)微枝嘶,所以先簡要回顧一下。

串行 vs. 并發(fā)

這兩個詞用來描述任務(wù)的執(zhí)行順序哑诊。 串行 在同一時間點總是單獨執(zhí)行一個任務(wù)群扶,而并發(fā)可以同時執(zhí)行多個任務(wù)。

任務(wù)

在本教程中,你可以把任務(wù)當(dāng)做一個閉包(closure)竞阐。實際上缴饭,你可以將GCD和函數(shù)指針一起使用,但是一般很少這樣使用骆莹。閉包更簡單颗搂!

不記得Swift中的閉包?閉包是自含的幕垦,可保存?zhèn)鬟f并被調(diào)用的代碼塊丢氢。當(dāng)調(diào)用的時候,他們的用法很像函數(shù)先改,可以有參數(shù)和返回值疚察。除此之外,閉包可以“捕獲”外部的變量盏道,也就是說稍浆,它可以看到并記住它自身被定義時的作用域變量。

Swift中的閉包和OC中的塊(block)類似甚至于他們幾乎就是可交換使用的猜嘱。唯一的限制在于OC中不能使用Swift獨有的特性衅枫,比如元組(tuple)。但OC中的塊可以安全的替換成Swift中的閉包朗伶。

同步 vs. 異步

這兩個詞描述的是函數(shù)何時將控制權(quán)返回給調(diào)用者弦撩,以及在返回時任務(wù)的完成情況。

同步函數(shù)只有在任務(wù)完成后才會返回论皆。

異步函數(shù)會立即返回益楼,不會等待任務(wù)完成。因此異步函數(shù)不會阻塞當(dāng)前線程点晴。

注意 -- 當(dāng)你讀到同步函數(shù)阻塞(block)當(dāng)前進(jìn)程或者函數(shù)是阻塞(blocking)函數(shù)時感凤,不要困惑!動詞阻塞(block)描述的是函數(shù)對當(dāng)前線程的影響粒督,和塊(block)沒有關(guān)系陪竿。同時記住GCD文檔中有關(guān)OC的block可以跟Swift的閉包互換。

臨界區(qū)(Critical Section)

這是一段不能并發(fā)執(zhí)行的代碼屠橄,也就是說兩個線程不可以同時執(zhí)行它族跛。這通常是因為這段代碼會修改共享的資源。否則锐墙,并發(fā)的進(jìn)程同時修改同一個變量會導(dǎo)致錯誤贰谣。

競態(tài)條件

當(dāng)兩個線程競爭同一資源時滥比,如果對資源的訪問順序敏感纽谒,就稱存在競態(tài)條件遂唧。競態(tài)條件可能產(chǎn)生在代碼檢查時不易被發(fā)現(xiàn)的不可預(yù)期行為夺脾。

死鎖

兩個或更多的線程因等待彼此完成而陷入的困境稱為死鎖。第一個線程無法完成因為它在等待第二個線程完成掏膏。但是第二個線程也無法完成因為它在等待第一個線程完成劳翰。

線程安全

線程安全的代碼是可以被多個線程或并發(fā)任務(wù)安全調(diào)用的,他不會造成任何問題(數(shù)據(jù)錯誤馒疹,崩潰等)佳簸。非線程安全的代碼在同一時間只能單獨執(zhí)行。一段線程安全的代碼如let a = ["thread-safe"]颖变。由于數(shù)組是只讀的生均,它可以被多個線程同時使用而不會引發(fā)問題。另一方面腥刹,var a = ["thread-unsafe"]是可變數(shù)組马胧。這意味著它不是線程安全的,因為多個線程可以同時獲取并修改這個數(shù)組衔峰,會得到不可預(yù)料的結(jié)果佩脊。非線程安全的變量和可變的數(shù)據(jù)結(jié)構(gòu)在同一時刻應(yīng)該只能被一個線程獲取。

上下文切換

上下文切換是在進(jìn)程中切換不同線程時保存和恢復(fù)程序執(zhí)行狀態(tài)的過程垫卤。這一過程在編寫多任務(wù)app時相當(dāng)常見威彰,但是會造成一些額外開支。

并發(fā) vs 并行

并發(fā)和并行經(jīng)常會被同時提起穴肘,所以值得通過簡短的解釋來區(qū)分彼此歇盼。

并發(fā)代碼中的單獨部分可以同時執(zhí)行。然而评抚,這要由系統(tǒng)來決定并發(fā)怎樣發(fā)生或是否發(fā)生豹缀。

多核設(shè)備通過并行來同時執(zhí)行多個線程;然而慨代,在單核設(shè)備中邢笙,必須要通過上下文切換來運行另一個線程或進(jìn)程。這一過程通常發(fā)生的很快以至于給人并行的假象侍匙。如下圖所示

盡管你可能在GCD之下編寫并發(fā)執(zhí)行的代碼氮惯,但仍由GCD來決定并行的需求有多大。

深層次的觀點是并發(fā)實際上是關(guān)乎*結(jié)構(gòu)*的丈积。當(dāng)你編寫GCD代碼時筐骇,你組織你的代碼來揭示出可以同時運行的工作债鸡,以及不可以同時運行的江滨。如果你想深入了解這個主題,猛擊Rob Pike(vimeo.com/49718712)厌均。

隊列

GCD提供了 調(diào)度隊列 (dispatch queues)來處理提交的任務(wù)唬滑;這些隊列管理著你向GCD提交的任務(wù)并且以先進(jìn)先出(FIFO)的順序來執(zhí)行任務(wù)。這保證了第一個加入隊列的任務(wù)第一個被執(zhí)行,第二個加入的任務(wù)第二個開始執(zhí)行晶密,以此類推擒悬。

所有調(diào)度隊列都是線程安全的從而讓你可以同時在多個線程中使用它們。當(dāng)你明白了調(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ù)同時進(jìn)入臨界區(qū)的危險途事;這保證了臨界區(qū)不會出現(xiàn)競態(tài)條件。因此如果進(jìn)入臨界區(qū)的唯一途徑就是通過向調(diào)度隊列提交任務(wù)擅羞,那么可以保證臨界區(qū)是安全的尸变。

并發(fā)隊列

并發(fā)隊列中的任務(wù)可以保證按進(jìn)入隊列的順序被執(zhí)行...僅此而已!任務(wù)可能以任意順序完成而且你不知道何時下一個任務(wù)會開始祟滴,或是任一時刻有多少任務(wù)在運行振惰。再一次,這完全取決于GCD垄懂。

下圖展示了四個并發(fā)任務(wù)的例子:


并發(fā)隊列

任務(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ù),常常伴隨有用戶可見的進(jìn)度指示器窖认。使用它來做計算豫柬,I/O,網(wǎng)絡(luò)扑浸,持續(xù)的數(shù)據(jù)填充等任務(wù)烧给。這個等級被設(shè)計成節(jié)能的。

? QOS_CLASS_BACKGROUND: background 等級表示那些用戶不會察覺的任務(wù)喝噪。使用它來執(zhí)行預(yù)加載础嫡,維護(hù)或是其它不需用戶交互和對時間不敏感的任務(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ù)。最佳的學(xué)習(xí)方式就是通過下面的例子悍及。

示例

因為這篇教程的目標(biāo)是使用GCD優(yōu)化程序以及在不同線程中安全的運行代碼闽瓢,所以你會以一個幾近完成的項目GooglyPuff來開始。

GooglyPuff是一個未優(yōu)化心赶,非線程安全的app扣讼,使用Core Image的人臉識別API在人臉上疊加金魚眼。初始圖像可以從圖片庫中選擇或是從網(wǎng)絡(luò)下載一組預(yù)定的圖片园担。

GooglyPuff_Swift_Start_1

一旦下載了工程届谈,提取到合適的地方,打開Xcode并運行它弯汰〖枭剑看起來如下:


注意到當(dāng)你選擇 Le Internet 選項來下載圖片時,一個UIAlertController提示框會過早的彈出咏闪。你會在教程的第二部分修復(fù)這個問題曙搬。

這個工程中有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中填充過多雜亂的方法而造成超負(fù)荷埂材;以至于經(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)視圖控制器載入更快了,隨后金魚眼會加入進(jìn)來冬骚。這給app帶來了很好的效果椅贱,因為你展示出圖片修改前后的變化。同時只冻,如果你試圖加載一張極其巨大的圖片庇麦,app不會因為加載視圖控制器而失去響應(yīng),這讓app有很好的適應(yīng)性喜德。

正如前面所提到的山橄,dispatch_async以閉包的形式向隊列中追加了一項任務(wù)并立即返回了。這項任務(wù)會在GCD決定的稍后時間執(zhí)行舍悯。當(dāng)你需要執(zhí)行網(wǎng)絡(luò)請求或在后臺執(zhí)行繁重的CPU任務(wù)時驾胆,使用dispatch_async不會阻塞當(dāng)前進(jìn)程。

何時使用何種隊列類型快速指南:

- 自定義順序隊列:當(dāng)你想順序執(zhí)行后臺任務(wù)并追蹤它時贱呐,這是一個很好的選擇丧诺。因為同時只有一個任務(wù)在執(zhí)行,因此消除了資源競爭奄薇。注意如果需要從方法中獲取數(shù)據(jù)驳阎,你必須內(nèi)置另一個閉包來得到它或者考慮使用dispatch_sync。

- 主隊列(順序):當(dāng)并發(fā)隊列中的任務(wù)完成需要更新UI的時候馁蒂,這是一個通常的選擇呵晚。為達(dá)此目的,需要在一個閉包中嵌入另一個閉包沫屡。同時饵隙,如果在主隊列中調(diào)用dispatch_async來返回主隊列,能保證新的任務(wù)會在當(dāng)前方法完成后再執(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ù)

仔細(xì)思考你的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)常有人因為單例不是線程安全的而憂慮悬包。這種擔(dān)憂是很有道理的,考慮到他們的用法:單例經(jīng)常被多個控制器同時使用馍乙。 PhotoManager 類是一個單例布近,所以你要仔細(xì)思考這個問題垫释。

思考兩種情形,初始化單例的過程和對他進(jìn)行讀寫的過程撑瞧。

先來看初始化棵譬。這看起來很簡單,因為Swift在全局域中初始化變量预伺。在Swift中订咸,全局變量在首次使用時被初始化,并且保證初始化是原子操作酬诀。也就是說脏嚷,初始化代碼被視為臨界區(qū)從而保證了初始化在其他線程使用全局變量之前就完成了。Swift是怎么做到的瞒御?其實父叙,Swift在幕后使用了GCD中的dispatch_once,詳見博客(https://developer.apple.com/swift/blog/?id=7)肴裙。

dispatch_once以線程安全的方式執(zhí)行且僅執(zhí)行一次閉包趾唱。如果一個線程正處于臨界區(qū)中 -- 被提交給dispatch_once的任務(wù) -- 其他線程會阻塞直到它完成。并且一旦它完成蜻懦,其他線程不會再執(zhí)行臨界區(qū)中的代碼甜癞。用let將單例定義為全局常量,我們可以進(jìn)一步保證變量在初始化后不會發(fā)生變化宛乃。從某種意義上說带欢,所有Swift全局常亮量都天生是單例,并且線程安全地初始化烤惊。

但是我們?nèi)孕枰紤]讀和寫乔煞。盡管Swift使用dispatch_once來確保單例初始化是線程安全的,但不能保證它所表示的數(shù)據(jù)類型也是線程安全的柒室。例如用一個全局變量來聲明一個類實例渡贾,但在類中還是會有修改類內(nèi)部數(shù)據(jù)的臨界區(qū)。此時就需要其他方式來達(dá)成線程安全雄右,比如通過對數(shù)據(jù)的同步化使用(synchronizing access)空骚。

處理讀寫問題

實例化線程安全性不是單例的唯一問題。如果單例的屬性表示一個可變對象擂仍,比如PhotoManager中的photos囤屹,那么你就需要考慮那個對象是否線程安全。

在Swift中任意用let聲明的常量都是只讀并且線程安全的逢渔。用var聲明的變量是可變且非線程安全的肋坚,除非數(shù)據(jù)類型本身被設(shè)計成線程安全。Swift中的集合類型比如Array和Dictionary,當(dāng)聲明為變量時不是線程安全的智厌。那么像Foundation的容器NSArray呢诲泌?是線程安全的嗎?答案是--“可能不是”铣鹏!Apple維護(hù)的一個幫助列表中有許多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ù)組的拷貝并且保護(hù)了原始數(shù)組不被改變栅受,但是這不能保證一個線程在調(diào)用addPhoto來寫的時候沒有另一個線程同時也在調(diào)用getter方法讀photos屬性将硝。

注意 :在上面的代碼中恭朗,為什么調(diào)用者要獲取photo數(shù)組的拷貝?在Swift中依疼,參數(shù)或函數(shù)返回是通過值或引用來傳遞的痰腮。引用傳遞和OC中的傳指針一樣,這意味著你得到的是原始的對象律罢,對這個對象的修改會影響到其他使用了這個對象引用的代碼膀值。值傳遞拷貝了對象本身,對拷貝的修改不會影響原始的對象误辑。默認(rèn)情況下沧踏,Swift類實例是引用傳遞而結(jié)構(gòu)體是值傳遞。 Swift內(nèi)置的數(shù)據(jù)類型巾钉,如Array和Dictionary翘狱,是用結(jié)構(gòu)體來實現(xiàn)的,看起來傳遞集合類型會造成代碼中出現(xiàn)大量的拷貝砰苍。不要因此擔(dān)心內(nèi)存使用問題潦匈。Swift的集合類型經(jīng)過優(yōu)化,只有在需要的時候才進(jìn)行拷貝赚导,比如通過值傳遞的數(shù)組在第一次被修改的時候茬缩。

這是軟件開發(fā)中經(jīng)典的讀者寫者問題(Readers-Writers Problem)。GCD使用 調(diào)度屏障 (dispatch barriers)提供了一個優(yōu)雅的解決方案來生成讀寫鎖吼旧。

當(dāng)跟并發(fā)隊列一起工作時凰锡,調(diào)度屏障是一族行為像序列化瓶頸的函數(shù)。使用GCD的barrier API確保了提交的閉包是指定隊列中在特定時段唯一在執(zhí)行的一個。也就是說必須在所有先于調(diào)度屏障提交的任務(wù)已經(jīng)完成的情況下寡夹,閉包才能開始執(zhí)行处面。

當(dāng)輪到閉包時,屏障執(zhí)行這個閉包并確保隊列在此過程不會執(zhí)行其他任務(wù)菩掏。一旦閉包完成魂角,隊列返回到默認(rèn)的執(zhí)行方式。GCD同時提供了同步和異步兩種屏障函數(shù)智绸。

下圖說明了屏障函數(shù)應(yīng)用于多個異步任務(wù)的效果:


注意隊列開始就像普通的并發(fā)隊列一樣工作野揪。但當(dāng)屏障執(zhí)行的時候,隊列變成像順序隊列一樣瞧栗。就是說斯稳,屏障是唯一一個在執(zhí)行的任務(wù)。在屏障完成后迹恐,隊列恢復(fù)成普通的并發(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命名習(xí)慣刺啦;保證描述性以利于調(diào)試留特。第二個參數(shù)指出你的隊列是順序的還是并發(fā)的。

注意 :當(dāng)在網(wǎng)上搜索例子時洪燥,你經(jīng)晨某樱看到人們傳0或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. 將寫操作加入自定義的隊列中。當(dāng)臨界區(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到當(dāng)前隊列中。這會造成死鎖坚芜。因為調(diào)用在等待閉包完成览芳,但是閉包無法完成(甚至根本沒開始P崩选)鸿竖,直到當(dāng)前在執(zhí)行的任務(wù)結(jié)束,但當(dāng)前任務(wù)沒法結(jié)束(因為阻塞的閉包還沒完成)铸敏!這就要求你必須清醒的認(rèn)識到你從哪個隊列調(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)學(xué)到了如何編寫線程安全的代碼以及如何在保持主線程響應(yīng)性的前提下執(zhí)行CPU密集型的任務(wù)。

可以下載GooglyPuff谍夭,里面包含了本教程中所做的所有改進(jìn)黑滴。教程的第二部分會在此基礎(chǔ)上繼續(xù)改進(jìn)。

如果你打算優(yōu)化自己的app紧索,你真的應(yīng)該使用 Instruments 中的**Time Profile** 模板來測試袁辈。使用方法已經(jīng)超出本教程范圍,可以查看怎樣使用Instruments珠漂。

同時確保你在真機上測試晚缩,因為在模擬器上測試會得到跟真實體驗相差甚遠(yuǎn)的結(jié)果。

在教程的下篇你會更深入GCD的API中做些更酷的事情媳危。

譯者:loveltyoic (本文轉(zhuǎn)自作者loveltyoic,很感謝!文章轉(zhuǎn)載需要注冊,只好復(fù)制過來自己編輯,如有冒犯或禁止轉(zhuǎn)載,望告知)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荞彼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子待笑,更是在濱河造成了極大的恐慌鸣皂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暮蹂,死亡現(xiàn)場離奇詭異寞缝,居然都是意外死亡,警方通過查閱死者的電腦和手機椎侠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門第租,熙熙樓的掌柜王于貴愁眉苦臉地迎上來措拇,“玉大人我纪,你說我怎么就攤上這事∝は牛” “怎么了浅悉?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長券犁。 經(jīng)常有香客問我术健,道長,這世上最難降的妖魔是什么粘衬? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拌夏,結(jié)果婚禮上绑青,老公的妹妹穿的比我還像新娘。我一直安慰自己跪腹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布飞醉。 她就那樣靜靜地躺著冲茸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缅帘。 梳的紋絲不亂的頭發(fā)上轴术,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音钦无,去河邊找鬼逗栽。 笑死,一個胖子當(dāng)著我的面吹牛失暂,可吹牛的內(nèi)容都是我干的祭陷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼趣席,長吁一口氣:“原來是場噩夢啊……” “哼兵志!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宣肚,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤想罕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霉涨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體按价,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年笙瑟,在試婚紗的時候發(fā)現(xiàn)自己被綠了楼镐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡往枷,死狀恐怖框产,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情错洁,我是刑警寧澤秉宿,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站屯碴,受9級特大地震影響描睦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜导而,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一忱叭、第九天 我趴在偏房一處隱蔽的房頂上張望隔崎。 院中可真熱鬧,春花似錦韵丑、人聲如沸仍稀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽技潘。三九已至,卻和暖如春千康,著一層夾襖步出監(jiān)牢的瞬間享幽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工拾弃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留值桩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓豪椿,卻偏偏與公主長得像奔坟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子搭盾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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