儘管Grand Central Dispatch(GCD)已經存在一段時間了,但並不是每一個人都知道怎麼使用它。這是情有可原的,由於併發很棘手,並且GCD自己基於C的API在 Swift世界中很刺眼。 在這兩篇教程中,你會學到GCD的前因後果。第一部分解釋了GCD能夠作什麼和幾個基本功能。第二部分,你會學到一些GCD所提供的進階功能。編程
起步swift
libdispatch 是Apple所提供的在IOS和OS X上進行併發編程的庫,而GCD正是它市場化的名字。GCD有以下優勢: – GCD能夠將計算複雜的任務放到後臺執行,從而提高app的響應性能 – GCD提供了比鎖和線程更簡單的併發模型,幫助開發者避免併發的bug。數組
爲了理解GCD,你須要瞭解一些線程和併發的概念。這些概念可能很含糊而且細微,因此先簡要回顧一下。安全
串行 vs 併發網絡
這兩個詞用來描述任務的執行順序。串行在同一時間點老是單獨執行一個任務,而併發能夠同時執行多個任務。數據結構
任務多線程
在本教程中,你能夠把任務當作一個閉包(closure)。實際上,你能夠將GCD和函數指針一塊兒使用,可是通常不多這樣使用。閉包更簡單!閉包
不記得Swift中的閉包?閉包是自含的,可保存傳遞並被調用的代碼塊。當調用的時候,他們的用法很像函數,能夠有參數和返回值。除此以外,閉包能夠「捕獲」外部的變量,也就是說,它能夠看到並記住它自身被定義時的做用域變量。併發
Swift中的閉包和OC中的塊(block)相似甚至於他們幾乎就是可交換使用的。惟一的限制在於OC中不能使用Swift獨有的特性,好比元組(tuple)。但OC中的塊能夠安全的替換成Swift中的閉包。app
同步 vs 異步
這兩個詞描述的是函數什麼時候將控制權返回給調用者,以及在返回時任務的完成狀況。
同步函數只有在任務完成後纔會返回。
異步函數會當即返回,不會等待任務完成。所以異步函數不會阻塞當前線程。
注意:當你讀到同步函數阻塞(block)當前進程或者函數是阻塞(blocking)函數時,不要困惑!動詞阻塞(block)描述的是函數對當前線程的影響,和塊(block)沒有關係。同時記住GCD文檔中有關OC的block能夠跟Swift的閉包互換。
臨界區(Critical Section)
這是一段不能併發執行的代碼,也就是說兩個線程不能夠同時執行它。這一般是由於這段代碼會修改共享的資源。不然,併發的進程同時修改同一個變量會致使錯誤。
競態條件
當兩個線程競爭同一資源時,若是對資源的訪問順序敏感,就稱存在競態條件。競態條件可能產生在代碼檢查時不易被發現的不可預期行爲。
死鎖
兩個或更多的線程因等待彼此完成而陷入的困境稱爲死鎖。第一個線程沒法完成由於它在等待第二個線程完成。可是第二個線程也沒法完成由於它在等待第一個線程完成。
線程安全
線 程安全的代碼是能夠被多個線程或併發任務安全調用的,他不會形成任何問題(數據錯誤,崩潰等)。非線程安全的代碼在同一時間只能單獨執行。一段線程安全的 代碼如let a = ["thread-safe"]。因爲數組是隻讀的,它能夠被多個線程同時使用而不會引起問題。另外一方面,var a = ["thread-unsafe"]是可變數組。這意味着它不是線程安全的,由於多個線程能夠同時獲取並修改這個數組,會獲得不可預料的結果。非線程安全 的變量和可變的數據結構在同一時刻應該只能被一個線程獲取。
上下文切換
上下文切換是在進程中切換不一樣線程時保存和恢復程序執行狀態的過程。這一過程在編寫多任務app時至關常見,可是會形成一些額外開支。
併發 vs 並行
併發和並行常常會被同時提起,因此值得經過簡短的解釋來區分彼此。
併發代碼中的單獨部分能夠同時執行。然而,這要由系統來決定併發怎樣發生或是否發生。
多核設備經過並行來同時執行多個線程;然而,在單核設備中,必需要經過上下文切換來運行另外一個線程或進程。這一過程一般發生的很快以致於給人並行的假象。以下圖所示:
儘管你可能在GCD之下編寫併發執行的代碼,但仍由GCD來決定並行的需求有多大。
深層次的觀點是併發其實是關乎結構的。當你編寫GCD代碼時,你組織你的代碼來揭示出能夠同時運行的工做,以及不能夠同時運行的。若是你想深刻了解這個主題,猛擊Rob Pike。
隊列
GCD提供了調度隊列(dispatch queues)來處理提交的任務;這些隊列管理着你向GCD提交的任務而且以先進先出(FIFO)的順序來執行任務。這保證了第一個加入隊列的任務第一個被執行,第二個加入的任務第二個開始執行,以此類推。
全部調度隊列都是線程安全的從而讓你能夠同時在多個線程中使用它們。當你明白了調度隊列如何爲你的代碼提供了線程安全性時,GCD的優勢就很明顯了。關鍵是選擇正確的調度隊列種類和正確的調度函數(dispatching function)來提交你的任務。
順序隊列
順序隊列中的任務同一時間只執行一件任務,每件任務只有在先前的任務完成後纔開始。同時,你並不知道一個任務完成到另外一個任務開始之間的間隔時間,以下圖所示:
任務的執行是在GCD掌控之下的;你惟一肯定的就是GCD在同一時刻只執行一件任務而且按任務加入隊列的順序執行。
由於不會在順序隊列中同時執行兩件任務,因此沒有多個任務同時進入臨界區的危險;這保證了臨界區不會出現競態條件。所以若是進入臨界區的惟一途徑就是經過向調度隊列提交任務,那麼能夠保證臨界區是安全的。
併發隊列
併發隊列中的任務能夠保證按進入隊列的順序被執行…僅此而已!任務可能以任意順序完成並且你不知道什麼時候下一個任務會開始,或是任一時刻有多少任務在運行。再一次,這徹底取決於GCD。 下圖展現了四個併發任務的例子:
任務1,2和3都運行的很快,一個接一個。可是任務1在任務0開始了一段時間後纔開始。同時,任務3在任務2開始後纔開始可是卻更早完成。
什麼時候開始一個任務徹底取決於GCD。若是一個任務的執行時間和另外一個的發生重疊,將由GCD來決定是否要將任務運行在另外一個可用的核上或是經過上下文切換來運行另外一個程序。
有趣的是,GCD爲每種隊列類型提供了至少5種特別的隊列。
隊列類型
首先,系統提供了一種特殊的順序隊列main queue。和其餘的順序隊列同樣,在這個隊列裏的任務同一時刻只有一個在執行。然而,這個隊列保證了全部任務會在主線程中執行,主線程是惟一一個容許更新UI的線程。這個隊列用來向UIView對象發消息或發通知。
系統同時提供了幾種併發隊列。這些隊列和它們自身的QoS等級相關。QoS等級表示了提交任務的意圖,使得GCD能夠決定如何制定優先級。
QOS_CLASS_USER_INTERACTIVE: user interactive等級表示任務須要被當即執行以提供好的用戶體驗。使用它來更新UI,響應事件以及須要低延時的小工做量任務。這個等級的工做總量應該保持較小規模。
QOS_CLASS_USER_INITIATED:user initiated等級表示任務由UI發起而且能夠異步執行。它應該用在用戶須要即時的結果同時又要求能夠繼續交互的任務。
QOS_CLASS_UTILITY:utility等級表示須要長時間運行的任務,經常伴隨有用戶可見的進度指示器。使用它來作計算,I/O,網絡,持續的數據填充等任務。這個等級被設計成節能的。
QOS_CLASS_BACKGROUND:background等級表示那些用戶不會察覺的任務。使用它來執行預加載,維護或是其它不需用戶交互和對時間不敏感的任務。
要清楚Apple的API同時也使用了全局調度隊列(global dispatch queue),因此你添加的任何任務都不是這些隊列中的惟一任務。
最後,你能夠建立自定義的順序或併發隊列。意味着你至少有5種隊列:主隊列(main queue),四種通用調度隊列,加上任意你本身定製的隊列!
以上就是調度隊列的主要部分!
GCD的「藝術」可歸結爲選擇正確的隊列調度函數來提交任務。最佳的學習方式就是經過下面的例子。
示例
由於這篇教程的目標是使用GCD優化程序以及在不一樣線程中安全的運行代碼,因此你會以一個幾近完成的項目GooglyPuff來開始。
GooglyPuff是一個未優化,非線程安全的app,使用Core Image的人臉識別API在人臉上疊加金魚眼。初始圖像能夠從圖片庫中選擇或是從網絡下載一組預約的圖片。
一旦下載了工程,提取到合適的地方,打開Xcode並運行它。看起來以下:
注意:當你選擇Le Internet選項來下載圖片時,一個UIAlertController提示框會過早的彈出。你會在教程的第二部分修復這個問題。
這 個工程中有4個須要關心的類: – PhotoCollectionViewController:app啓動後的第一個視圖控制器。展現全部選擇的圖片的縮略圖。 – PhotoDetailViewController:爲圖片加上金魚眼並在UIScrollView中展現。 – Photo:描述圖片屬性的協議。提供圖片,縮略圖和狀態。兩個類實現了這個協議:DownloadPhoto從NSURL實例化圖 片,AssetPhoto從ALAsset實例化圖片。 – PhotoManager:管理全部Photo對象。
使用dispatch_sync處理後臺任務
返回app並從圖片庫中添加一些圖片或使用Le Internet選項下載一些。
留意在輕觸PhotoCollectionViewController中的UICollectionViewCell後要多久才能完成PhotoDetailViewController的初始化;此時存在明顯的延遲,尤爲是在較慢的設備上瀏覽較大的圖片時。
一不當心就會在UIViewController的viewDidLoad中填充過多雜亂的方法而形成超負荷;以致於常常要等待好久視圖控制器纔會出現。若是可能的話,最好將一些工做轉移到後臺去完成,若是這些工做在加載時不是必需的。
聽起來是使用dispatch_async的時候!
打開PhotoDetailViewController而後用下面的實現替換viewDidload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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. 首先將工做從主線程上轉移到全局隊列中。由於這是一個dispatch_async調用,異步提交的閉包意味着調用線程會繼續執行下去。這使得 viewDidLoad在主線程上更早的完成從而讓加載的過程在感受上更迅速。同時,人臉識別過程已經開始並會在晚些時候完成。 2. 在這時,人臉識別已經完成並生成一張新圖片。由於要用這張新圖片更新UIImageView,因此把一個閉包加入主線程中。記住 — 必須老是在主線程中操做UIKit! 3. 最後,用fadeInNewImage更新UI。
注意:你在使用Swift的尾隨閉包(trailing closure)語法,將閉包寫在參數括號的後面傳給dispatch_async。這種語法看起來更清晰,由於閉包沒有內嵌到函數括號中。
運行app;選擇一張圖片而後你會明顯地發現視圖控制器載入更快了,隨後金魚眼會加入進來。這給app帶來了很好的效果,由於你展現出圖片修改先後的變化。同時,若是你試圖加載一張極其巨大的圖片,app不會由於加載視圖控制器而失去響應,這讓app有很好的適應性。
正如前面所提到的,dispatch_async以閉包的形式向隊列中追加了一項任務並當即返回了。這項任務會在GCD決定的稍後時間執行。當你須要執行網絡請求或在後臺執行繁重的CPU任務時,使用dispatch_async不會阻塞當前進程。
何 時使用何種隊列類型快速指南: – 自定義順序隊列:當你想順序執行後臺任務並追蹤它時,這是一個很好的選擇。由於同時只有一個任務在執行,所以消除了資源競爭。注意若是須要從方法中獲取數 據,你必須內置另外一個閉包來獲得它或者考慮使用dispatch_sync。 – 主隊列(順序):當併發隊列中的任務完成須要更新UI的時候,這是一個一般的選擇。爲達此目的,須要在一個閉包中嵌入另外一個閉包。同時,若是在主隊列中調 用dispatch_async來返回主隊列,能保證新的任務會在當前方法完成後再執行。 – 併發隊列:一般用來執行與UI無關的後臺任務。
獲取全局隊列的幫助變量(Helper Variable)
你 可能注意到dispatch_get_global_queue的QoS等級參數寫起來有些繁瑣。這是因爲qos_class_t被定義爲一個結構體,它 包含有Uint32型的屬性value,而這個屬性須要被轉型爲Int。在Utils.swift中添加一些全局的計算變量,使獲取全局隊列更方便一些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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替換爲幫助變量:
1
2
3
4
5
6
|
dispatch_async(GlobalUserInitiatedQueue) {
let overlayImage = self.faceOverlayImageFromImage(self.image)
dispatch_async(GlobalMainQueue) {
self.fadeInNewImage(overlayImage)
}
}
|
這使得調度調用更易讀而且很容易看出在使用哪一個隊列。
用dispatch_after推遲任務
仔細思考你的app中的UX。用戶可能在第一次打開app的時候不知道該作什麼,不是嗎?
若是在PhotoManager類中沒有圖片的時候,給用戶一個提示是個不錯的主意。然而,你同時要考慮用戶的視線怎樣掃過屏幕:若是提示出現的太快,用戶可能還在看其餘的地方而忽略了提示。
推遲一秒鐘再出現提示,此時即可抓住用戶的注意力,由於他們已經對app有了第一印象。
將下面的代碼加到showOrHideNavPrompt的實現中,它位於PhotoCollectionViewController.swift文件底部。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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從新加載的時候被執行。代碼解釋以下: 1. 聲明推遲的時間。 2. 等待delayInSeconds所表示的時間,而後將閉包異步地加入主隊列中。
運行app。在短暫的延遲後,提示會出現並吸引用戶的注意。
dispatch_after的工做原理就像推遲的dispatch_async。一旦dispatch_after返回,你仍是沒法掌握實際的執行時間抑或是取消任務。
想知道什麼時候使用dispatch_after?
自定義順序隊列:慎用。在自定義順序隊列中慎用dispatch_after。你最好留在主隊列中。
主隊列(順序):好主意。在主隊列中使用dispatch_after是一個好主意;Xcode對此有自動補全模板。
併發隊列:慎用。不多會這樣使用,最好留在主隊列中。
單例和線程安全
單例。愛也好,恨也罷,它們在iOS中就像貓之於互聯網同樣流行。
常常有人由於單例不是線程安全的而憂慮。這種擔心是頗有道理的,考慮到他們的用法:單例常常被多個控制器同時使用。PhotoManager類是一個單例,因此你要仔細思考這個問題。
思考兩種情形,初始化單例的過程和對他進行讀寫的過程。
先 來看初始化。這看起來很簡單,由於Swift在全局域中初始化變量。在Swift中,全局變量在首次使用時被初始化,而且保證初始化是原子操做。也就是 說,初始化代碼被視爲臨界區從而保證了初始化在其餘線程使用全局變量以前就完成了。Swift是怎麼作到的?其實,Swift在幕後使用了GCD中的 dispatch_once,詳見博客。
dispatch_once 以線程安全的方式執行且僅執行一次閉包。若是一個線程正處於臨界區中 — 被提交給dispatch_once的任務 — 其餘線程會阻塞直到它完成。而且一旦它完成,其餘線程不會再執行臨界區中的代碼。用let將單例定義爲全局常量,咱們能夠進一步保證變量在初始化後不會發 生變化。從某種意義上說,全部Swift全局常亮量都天生是單例,而且線程安全地初始化。
可是咱們仍須要考慮讀和寫。儘管Swift使用 dispatch_once來確保單例初始化是線程安全的,但不能保證它所表示的數據類型也是線程安全的。例如用一個全局變量來聲明一個類實例,但在類中 仍是會有修改類內部數據的臨界區。此時就須要其餘方式來達成線程安全,好比經過對數據的同步化使用(synchronizing access)。
處理讀寫問題
實例化線程安全性不是單例的惟一問題。若是單例的屬性表示一個可變對象,好比PhotoManager中的photos,那麼你就須要考慮那個對象是否線程安全。
在 Swift中任意用let聲明的常量都是隻讀而且線程安全的。用var聲明的變量是可變且非線程安全的,除非數據類型自己被設計成線程安全。Swift中 的集合類型好比Array和Dictionary,當聲明爲變量時不是線程安全的。那麼像Foundation的容器NSArray呢?是線程安全的嗎? 答案是—「可能不是」!Apple維護的一個幫助列表中有許多Foundation中非線程安全的類。
儘管不少線程能夠同時讀取一個Array的可變實例而不出問題,但若是一個線程在修改數組的同時另外一個線程卻在讀取這個數組,這是不安全的。你的單例目前還不能阻止這種狀況發生。
爲了弄清楚問題,看看PhotoManager.swift中的addPhoto:
1
2
3
4
5
6
|
func addPhoto(photo: Photo) {
_photos.append(photo)
dispatch_async(dispatch_get_main_queue()) {
self.postContentAddedNotification()
}
}
|
這是一個寫方法,由於它修改了一個可變數組。
再看看photos屬性:
1
2
3
4
|
private
var
_photos: [Photo] = []
var
photos: [Photo] {
return
_photos
}
|
這個屬性的getter方法是一個讀方法。調用者獲得一個數組的拷貝而且保護了原始數組不被改變,可是這不能保證一個線程在調用addPhoto來寫的時候沒有另外一個線程同時也在調用getter方法讀photos屬性。
注意:
在 上面的代碼中,爲何調用者要獲取photo數組的拷貝?在Swift中,參數或函數返回是經過值或引用來傳遞的。引用傳遞和OC中的傳指針同樣,這意味 着你獲得的是原始的對象,對這個對象的修改會影響到其餘使用了這個對象引用的代碼。值傳遞拷貝了對象自己,對拷貝的修改不會影響原始的對象。默認狀況 下,Swift類實例是引用傳遞而結構體是值傳遞。
Swift內置的數據類型,如Array和Dictionary,是用結構體來實現的,看起來傳遞集合類型會形成代碼中出現大量的拷貝。不要所以擔憂內存使用問題。Swift的集合類型通過優化,只有在須要的時候才進行拷貝,好比經過值傳遞的數組在第一次被修改的時候。
這是軟件開發中經典的讀者寫者問題(Readers-Writers Problem)。GCD使用調度屏障(dispatch barriers)提供了一個優雅的解決方案來生成讀寫鎖。
當跟併發隊列一塊兒工做時,調度屏障是一族行爲像序列化瓶頸的函數。使用GCD的barrier API確保了提交的閉包是指定隊列中在特定時段惟一在執行的一個。也就是說必須在全部先於調度屏障提交的任務已經完成的狀況下,閉包才能開始執行。
當輪到閉包時,屏障執行這個閉包並確保隊列在此過程不會執行其餘任務。一旦閉包完成,隊列返回到默認的執行方式。GCD同時提供了同步和異步兩種屏障函數。
下圖說明了屏障函數應用於多個異步任務的效果:
注意隊列開始就像普通的併發隊列同樣工做。但當屏障執行的時候,隊列變成像順序隊列同樣。就是說,屏障是惟一一個在執行的任務。在屏障完成後,隊列恢復成普通的併發隊列。
下面說明何時用 — 何時不該該用 — 屏障函數:
自定義順序隊列:壞選擇。由於順序隊列自己就是順序執行,屏障不會起到任何幫助做用。
全局併發隊列:慎用。其餘系統可能也在使用隊列,你不該該出於自身目的而獨佔隊列。
自定義併發隊列:最佳選擇。用於原子操做或是臨界區代碼。任何須要線程安全的設置和初始化均可以使用屏障。
由於以上惟一合適的選擇就是自定義併發隊列,你須要生成一個這樣的隊列來處理屏障函數以隔離讀寫操做。併發隊列容許多個線程同時的讀操做。
打開PhotoManager.swift並在photos屬性下面添加以下私有屬性到類中:
1
2
|
private let concurrentPhotoQueue = dispatch_queue_create(
"com.raywenderlich.GooglyPuff.photoQueue"
, DISPATCH_QUEUE_CONCURRENT)
|
使用dispatch_queue_create初始化一個併發隊列concurrentPhotoQueue。第一個參數遵循反向DNS命名習慣;保證描述性以利於調試。第二個參數指出你的隊列是順序的仍是併發的。
注意:當在網上搜索例子時,你常常看到人們傳0或NULL做爲dispatch_queue_create的第二個參數。這是一種過期的方法來生成順序調度隊列;最好用參數顯示聲明。
找到addPhoto並用以下實現替換之:
1
2
3
4
5
6
7
8
|
func addPhoto(photo: Photo) {
dispatch_barrier_async(concurrentPhotoQueue) {
// 1
self._photos.append(photo)
// 2
dispatch_async(GlobalMainQueue) {
// 3
self.postContentAddedNotification()
}
}
}
|
來 看這段代碼如何工做的: 1. 將寫操做加入自定義的隊列中。當臨界區被執行時,這是隊列中惟一一個在執行的任務。 2. 將對象加入數組。由於是屏障閉包,這個閉包不會和concurrentPhotoQueue中的其餘任務同時執行。 3. 最終發送一個添加了圖片的通知。這個通知應該在主線程中發送由於這涉及到UI,因此這裏分派另外一個異步任務到主隊列中。
這個任務解決了寫問題,可是你還須要實現photos的讀方法。
爲確保和寫操做保持線程安全,你須要在concurrentPhotoQueue中執行讀操做。可是你須要從函數返回讀數據,因此不能異步地提交讀操做到隊列裏,由於異步任務不能保證在函數返回前執行。
所以,dispatch_sync是個極好的候選。
dispatch_sync同步提交任務並等到任務完成後才返回。使用dispatch_sync和調度屏障一塊兒來跟蹤任務;或是在須要等待返回數據時使用dispatch_sync。
仍 需當心。設想你調用dispatch_sync到當前隊列中。這會形成死鎖。由於調用在等待閉包完成,可是閉包沒法完成(甚至根本沒開始!),直到當前在 執行的任務結束,但當前任務無法結束(由於阻塞的閉包還沒完成)!這就要求你必須清醒的認識到你從哪一個隊列調用了閉包,以及你將任務提交到哪一個隊列。
概 述一下什麼時候何地使用dispatch_sync: – 自定義順序隊列:很是當心;若是你在運行一個隊列時調用dispatch_sync調度任務到同一個隊列,你顯然會製造死鎖。 – 主隊列(順序):很是當心,原理同上。 – 併發隊列:好選擇。用在和調度屏障同步或是等待任務完成以繼續後續處理。 仍是在PhotoManager.swift中,替換photos以下:
1
2
3
4
5
6
7
|
var
photos: [Photo] {
var
photosCopy: [Photo]!
dispatch_sync(concurrentPhotoQueue) {
// 1
photosCopy = self._photos
// 2
}
return
photosCopy
}
|
分別來看每一個號碼註釋: 1. 同步調度到concurrentPhotoQueue隊列執行讀操做。 2. 保存圖片數組的拷貝到photoCopy並返回它。
恭喜 —— 你的PhotoManager單例已是線程安全的了。不論你讀或是寫圖片數組,你都有信心保證操做會安全的執行。
回顧
仍是不能100%的肯定GCD的本質?你能夠本身建立使用GCD函數的簡單例子,經過斷點和NSLog來確保你明白髮生了什麼。
我這裏有兩張動態GIF圖片來幫助你理解dispatch_async和dispatch_sync。每張GIF上面都有代碼輔助你理解;注意代碼中的斷點和相應的隊列狀態。
重訪dispatch_sync
1
2
3
4
5
6
7
8
9
10
11
12
|
override func viewDidLoad() {
super
.viewDidLoad()
dispatch_sync(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
NSLog(
"First Log"
)
}
NSLog(
"Second Log"
)
}
|
下面對圖片中的幾個狀態作說明:
1. 主隊列循序漸進的執行任務 —— 緊接着的任務是實例化包含viewDidLoad的UIViewController類。
2. viewDidLoad在主線程中執行。
3. dispatch_sync閉包被加入到全局隊列中稍後執行。主線程停下來等待閉包完成。同時,全局隊列正在併發執行任務;記住閉包以FIFO的順序從全 局隊列中取出,可是會併發地執行。全局隊列首先處理dispatch_sync閉包加入前已經存在隊列中的任務。
4. 最後,輪到dispatch_sync閉包執行。
5. 閉包執行完畢,主線程得以繼續。
6. viewDidLoad方法完成,主隊列接着處理其它任務。
dispatch_sync把任務加入隊列並一直等待其完成。dispatch_async作了差很少的工做,只是它不會等待任務完成,而是轉而去繼續其餘工做。
重訪dispatch_async
1
2
3
4
5
6
7
8
9
10
11
12
|
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.主隊列循序漸進的執行任務 —— 緊接着的任務是實例化包含viewDidLoad的UIViewController類。
2.viewDidLoad在主線程中執行。
3.dispatch_async閉包被加入到全局隊列中稍後執行。
4.viewDidLoad在dispatch_async後繼續向下執行,主線程繼續其餘任務。同時,全局隊列正在併發執行任務;記住閉包以FIFO的順序從全局隊列中取出,可是會併發地執行。
5.執行dispatch_async所添加的閉包。
6.dispatch_async閉包完成,NSLog輸出到控制檯。
在這個特別的例子中,第一個NSLog在第二個NSLog後執行。事實並不是老是如此——這取決於硬件在彼時正在作什麼,你沒法控制或知曉哪一個語句會先執行。「第一個」NSLog在某種調用狀況下可能會先執行。
下一步?
在本教程中,你已經學到了如何編寫線程安全的代碼以及如何在保持主線程響應性的前提下執行CPU密集型的任務。
能夠下載GooglyPuff,裏面包含了本教程中所作的全部改進。教程的第二部分會在此基礎上繼續改進。