本文轉載,原文地址:http://www.cocoachina.com/ios/20150609/12072.html html
原文 Grand Central Dispatch Tutorail for Swift: Part 1/2
ios
原文做者:Bjrn Olav Ruudswift
譯者:Ethan Joe數組
雖然Grand Central Dispatch(下面簡稱爲GCD)已推出一段時間了。但並不是所有人都明確其原理。固然這是可以理解的,畢竟程序的併發機制很是繁瑣,而且基於C的GCD的API對於Swift的新世界並不是特別友好。xcode
在接下來的兩節教程中。你將學習GCD的輸入 (in)與輸出 (out)。安全
第一節將解釋什麼是GCD並瞭解幾個GCD的基礎函數。在第二節,你將學習幾個更加進階的GCD函數。網絡
Getting Started數據結構
GCD是libdispatch的代名詞。libdispatch表明着執行iOS與OS X的多核設備上執行並行代碼的官方代碼庫。多線程
它經常有下面幾個特色:閉包
GCD經過將高代價任務推遲執行並調至後臺執行的方式來提高App的交互速度。
GCD提供比鎖與多線程更簡單的併發模型,以此來避免一些由併發引發的Bug。
爲了理解GCD,你需要明確一些與線程、併發的相關的概念。這些概念間有着細微且模糊的區別,因此在學習GCD前請簡略地熟悉一下這些概念。
連續性 VS 併發性
這些術語用來描寫敘述一些被運行的任務彼此間的關係。
連續性運行任務表明着同一時間內僅僅運行一個任務。而併發性運行任務則表明着同一時間內可能會運行多個任務。
任務
在這篇教程中你可以把每個任務當作是一個閉包。 其實,你也可以經過函數指針來使用GCD。但在大多數狀況下這明顯有些麻煩。因此,閉包用起來更簡單。
不知道什麼是Swift中的閉包?閉包是可被儲存並傳值的可調用代碼塊,當它被調用時可以像函數那樣包括參數並返回值。
Swift中的閉包和Objective-C的塊很是相近,它們彼此間是可以相互交替的。這個過程當中有一點你不能作的是:用Objective-C的塊代碼去交互具備Swift獨有屬性屬性的閉包,比方說具備元組屬性的閉包。
但是從Swift端交互Objective-C端的代碼則是毫無障礙的,因此無論什麼時候你在文檔中看到到的Objective-C的塊代碼都是可用Swift的閉包取代的。
同步 VS 異步
這些術語用來描寫敘述當一個函數的控制權返回給調用者時已完畢的工做的數量。
同步函數僅僅有在其命令的任務完畢時纔會返回值。
異步函數則不會等待其命令的任務完畢。即會立刻返回值。因此,異步函數不會鎖住當前線程使其不能向隊列中的下一位函數運行。
值得注意的是---當你看到一個同步函數鎖住(block)了當前進程,或者一個函數是鎖函數(blocking function)或是鎖運算(block operation)時別認混了。這裏的鎖(blocks)是用來形容其對於本身線程的影響。它跟Objective-C中的塊(block)是不同的。再有一點要記住的就是在不論什麼GCD文檔中涉及到Objective-C的塊代碼都是可以用Swift的閉包來替換的。
臨界區
這是一段不能被在兩個線程中同一時候運行的代碼。
這是因爲這段代碼負責管理像變量這樣的若被併發進程使用便會更改的可共享資源。
資源競爭
這是一種軟件系統在一種不被控制的模式下依靠於特定隊列或者基於事件運行時間進行運行的狀況。比方說程序當前多個任務運行的詳細順序。
資源競爭可以產生一些不會在代碼排錯中立刻找到的錯誤。
死鎖
兩個或兩個以上的進程因等待彼此完畢任務或因執行其它任務而中止當前進程執行的狀況被稱做爲死鎖。舉個樣例,進程A因等待進程B完畢任務而中止執行,但進程B也在等待進程A完畢任務而中止執行的僵持狀態就是死鎖。
線程安全性
具備線程安全性的代碼可以在不產生不論什麼問題(比方數據篡改、崩潰等)的狀況下在多線程間或是併發任務間被安全的調用。不具備線程安全性的代碼的正常執行僅僅有在單一的環境下才可被保證。舉個具備線性安全性的代碼演示樣例let a = ["thread-safe"]。你可以在多線程間,不產生不論什麼bug的狀況下調用這個具備僅僅讀性的數組。相反,經過var a = ["thread-unsafe"]聲明的數組是可變可改動的。
這就意味着這個數組在多線層間可被改動從而產生一些不可預測的問題。對於那些可變的變量與數據結構最好不要同一時候在多個線程間使用。
上下文切換
上下文切換是當你在一個進程中的多個不一樣線程間進行切換時的一種進程進行儲存與恢復的狀態。這樣的進程在寫多任務App時至關常見。但這通常會產生額外的系統開銷。
併發 VS 並行
併發和並行老是被同一時候說起,因此有必要解釋一下二者間的差異。
併發代碼中各個單獨部分可以被"同一時候"執行。不管如何,這都由系統決定以何種方式執行。
具備多核處理器的設備經過並行的方式在同一時間內實現多線程間的工做;但是單核處理器設備僅僅能在同一時間內執行在單一線程上,並利用上下文切換的方式切換至其它線程以達到跟並行一樣的工做效果。例如如下圖所看到的。單核處理器設備執行速度快到造成了一種並行的假象。
併發 VS 並行
雖然你會在GCD下寫出使用多線程的代碼,但這仍由GCD來決定是否會使用併發機制。
並行機制包括着併發機制,但併發機制卻不必定能保證並行機制的執行。
隊列
GCD經過隊列分配的方式來處理待運行的任務。這些隊列管理着你提供給GCD待處理的任務並以FIFO的順序進行處理。
這就得以保證第一個加進隊列的任務會被首個處理,第二個加進隊列的任務則被其次處理,其後則以此類推。
連續隊列
連續隊列中的任務每次運行僅僅一個,一個任務僅僅有在其前面的任務運行完成後纔可開始運行。
例如如下圖所看到的。你不會知道前一個任務結束到下一個任務開始時的時間間隔。
連續隊列
每一個任務的運行時間都是由GCD控制的;惟一一件你可以確保的事即是GCD會在同一時間內依照任務加進隊列的順序運行一個任務。
因爲在連續隊列中不一樣意多個任務同一時候執行,這就下降了同一時候訪問臨界區的風險;這樣的機制在多任務的資源競爭的過程當中保護了臨界區。
假如分配任務至分發隊列是訪問臨界區的惟一方式。那這就保證了的臨界區的安全。
併發隊列
併發隊列中的任務依然以FIFO順序開始執行。
。。但你能知道的也就這麼多了!任務間可以以不論什麼順序結束,你不會知道下一個任務開始的時間也不會知道一段時間內正在執行任務的數量。因爲,這一切都是由GCD控制的。
例如如下圖所看到的。在GCD控制下的四個併發任務:
併發隊列
需要注意的是,在任務0開始運行後花了一段時間後任務1才開始運行。但任務一、二、3便一個接一個地高速運行起來。
再有。即使任務3在任務2開始運行後才開始運行,但任務3卻更早地結束運行。
任務的開始執行的時間全然由GCD決定。
假如一個任務與還有一個任務的執行時間相互重疊,便由GCD決定(在多核非繁忙可用的狀況下)是否利用不一樣的處理器執行或是利用上下文切換的方式執行不一樣的任務。
爲了用起來有趣一些,GCD提供了至少五種特別的隊列來相應不一樣狀況。
隊列種類
首先。系統提供了一個名爲主隊列(main queue)的特殊連續隊列。像其它連續隊列同樣,這個隊列在同一間內僅僅能運行一個任務。
不管如何,這保證了所有任務都將被這個惟一被贊成刷新UI的線程所運行。
它也是惟一一個用做向UIView對象發送信息或推送監聽(Notification)。
GCD也提供了其它幾個併發隊列。這幾個隊列都與本身的QoS (Quality of Service)類所關聯。Qos表明着待處理任務的運行意圖。GCD會依據待處理任務的運行意圖來決定最優化的運行優先權。
QOS_CLASS_USER_INTERACTIVE: user interactive類表明着爲了提供良好的用戶體驗而需要被立刻執行的任務。
它經常用來刷新UI、處理一些要求低延遲的載入工做。在App執行的期間,這個類中的工做完畢總量應該很是小。
QOS_CLASS_USER_INITIATED:user initiated類表明着從UI端初始化並可異步執行的任務。它在用戶等待及時反饋時和涉及繼續執行用戶交互的任務時被使用。
QOS_CLASS_UTILITY:utility類表明着長時間執行的任務。尤爲是那種用戶可見的進度條。它經常用來處理計算、I/O、網絡通訊、持續數據反饋及類似的任務。
這個類被設計得具備高效率處理能力。
QOS_CLASS_BACKBROUND:background類表明着那些用戶並不需要立刻知曉的任務。它經常用來完畢預處理、維護及一些不需要用戶交互的、對完畢時間並沒有過高要求的任務。
要知道蘋果的API也會使用這些全局分配隊列,因此你分派的任務不會是隊列中的惟一一個。
最後。你也可以本身寫一個連續隊列或是併發隊列。
算起來你起碼最少會有五個隊列:主隊列、四個全局隊列再加上你本身的隊列。
以上即是分配隊列的全體成員。
GCD的關鍵在於選擇正確的分發函數以此把你的任務分發至隊列。
理解這些東西的最好辦法就是無缺如下的Sample Project。
Sample Project
既然這篇教程的目的在於經過使用GCD在不一樣的線程間安全地調用代碼,那麼接下來的任務即是完畢這個名爲GooglyPuff的半成品。
GooglyPuff是一款經過CoreImage臉部識別API在照片中人臉的雙眼的位置上貼上咕嚕式的大眼睛且線程不安全的App。你既可以從Photo Library中選擇照片,也可以經過網絡從事先設置好的地址下載照片。
將project下載至本地後用Xcode打開並編譯執行。它看起來是這種:
GooglyPuff
在project中共同擁有四個類文件:
PhotoCollectionViewController:這是App執行後顯示的首個界面。它將顯示所有被選照片的縮略圖。
PhotoDetailViewController:它將處理將咕嚕眼加入至照片的工做並將處理完成的照片顯示在UIScrollView中。
Photo:一個包括着照片基本屬性的協議,當中有image(未處理照片)、thumbnail(裁減後的照片)及status(照片能否使用狀態);兩個用來實現協議的類,DownloadPhoto將從一個NSURL實例中實例化照片。而AssetPhoto則從一個ALAsset實例中實例化照片。
PhotoManager:這個類將管理所有Photo類型對象。
使用dispatch_async處理後臺任務
回到剛纔執行的App後,經過本身的Photo Library加入照片或是使用Le internet下載一些照片。
需要注意的是當你點擊PhotoCollectionViewController中的一個UICollectionViewCell後,界面切換至一個新的PhotoDetailViewController所用的時間。對於那些處理速度較慢的設備來講,處理一張較大的照片會產生一個很明顯的延遲。
這樣的狀況下很是easy使UIViewController的viewDidLoad因處理過於混雜的工做而負載;這麼作的結果便在view controller出現前產生較長的延遲。假如可能的話。咱們最好將某些工做放置後臺處理。
這聽起來dispatch_async該上場了。
打開PhotoDetailViewController後將viewDidLoad函數替換成下述代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
}
}
}
|
在這裏解釋一下上面改動的代碼:
你首先將照片處理工做從主線程(main thread)移至一個全局隊列(global queue)。
因爲這是一個異步派發(dispatch_async的調用,閉包以異步的形式進行傳輸意味着調用的線程將會被繼續運行。這樣一來便會使viewDidLoad更早的在主線程上結束運行並使得整個載入過程更加流暢。與此同一時候。臉部識別的過程已經開始並在一段時間後結束。
這時臉部識別的過程已經結束並生成了一張新照片。
當你想用這張新照片來刷新你的UIImageView時,你可以向主線程加入一個新的閉包。
需要注意的是--主線程僅僅能用來訪問UIKit。
最後。你便用這張有着咕嚕眼的fadeInNewImage照片來刷新UI。
有沒有注意到你已經用了Swift的跟隨閉包語法(trailing closure syntax),就是以在包括着特定分配隊列參數的括號後書寫表達式的形式了向dispatch_async傳遞閉包。假如把閉包寫出函數括號的話,語法會看起來更加簡潔。
執行並編譯App。選一張照片後你會發現view controller載入得很是快。咕嚕眼會在很是短的延遲後出現。現在的執行效果看起來比以前的好多了。當你嘗試載入一張大得離譜的照片時。App並不會在view controller載入時而延遲,這樣的機制便會使App表現得更加良好。
綜上所述,dispatch_async將任務以閉包的形式加入至隊列後立刻返回。這個任務在以後的某個時間段由GCD所運行。當你要在不影響當前線程工做的前提下將基於網絡或高密度CPU處理的任務移至後臺處理時,dispatch_asnyc便派上用場了。
接下來是一個關於在使用dispatch_asnyc的前提下,怎樣使用以及什麼時候使用不一樣類型隊列的簡潔指南:
本身定義連續隊列(Custom Serial Queue): 在當你想將任務移至後臺繼續工做並且時刻監測它的狀況下,這是一個不錯的選擇。
需要注意的是當你想從一個方法中調用數據時,你必須再加入一個閉包來回調數據或者考慮使用dispatch_sync。
主隊列(Main Queue[Serial]):這是一個當併發隊列中的任務完畢工做時來刷新UI的廣泛選擇。
爲此你得在一個閉包中寫入還有一個閉包。
固然。假如你已經在主線程並調用一個面向主線程的dispatch_async的話,你需要保證這個新任務在當前函數執行結束後的某個時間點開始執行。
併發隊列(Concurrent Queue):對於要執行後臺的非UI工做是個廣泛的選擇。
獲取全局隊列的簡潔化變量
你或許注意到了dispatch_get_global_queue函數裏的QoS類的參數寫起來有些麻煩。這是因爲qos_class_t被定義成一個值類型爲UInt32且最後還要被轉型爲Int的結構體。咱們可以在Utils.swift中的URL變量如下加入一些全局的簡潔化變量,以此使得調用全局隊列更加簡便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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有沒有使得用戶在第一次打開App的時候不知道該幹些什麼而感到不知所措呢?: ]
假如在PhotoManager中沒有不論什麼一張照片的時候便向用戶發出提醒應該是一個不錯的主意。不管如何,你仍是要考慮一下用戶在App主頁面上的注意力:假如你的提醒顯示得過快的話,用戶沒準在因爲看着其它地方而錯過它。
當用戶第一次使用App的時候,在提醒顯示前運行一秒鐘的延遲應該足以吸引住用戶的注意力。
在PhotoCollectionViewController.swift底部的showOrHideBarPrompt函數中加入例如如下代碼:
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!"
}
}
}
|
當你的UICollectionView重載的時候,viewDidLoad函數中的showOrHideNavPrompt將被運行。
解釋例如如下:
你聲明瞭一個表明詳細延遲時間的變量。
你將等待delayInSeconds變量中設定的時間而後向主隊列異步加入閉包。
編譯並執行App。你會看到一個在很是大程度上吸引用戶注意力並告知他們該作些什麼的細微延遲。
dispatch_after就像一個延遲的dispatch_async。你仍舊在實時執行的時候毫無操控權並且一旦dispatch_after返回後你也沒法取消整個延遲任務。
還在思考怎樣適當的使用dispatch_after?
本身定義連續隊列(Custom Serial Queue):當你在本身定義連續隊列上使用dispatch_after時必定要小心,此時最好不要放到主隊列上運行。
主隊列(Main Queue[Serial]):這對於dispatch_after是個很是好的選擇;Xcode對此有一個不錯的本身主動運行至完畢的樣板。
併發隊列(Concurrent Queue):在本身定義併發隊列上使用dispatch_after時相同要小心,即使你很是少這麼作。
此時最好放到主隊列上運行。
單例和線程安全
單例。不管你love it仍是hate it,他們對於iOS都是很重要的。: ]
一提到單例(Singleton)人們便認爲他們是線程不安全的。
這麼想的話也不是沒有道理:單例的實例經常在同一時間內被多線程所訪問。
PhotoManager類即是一個單例,因此你要思考一下上面提到的問題。
兩個需要考慮的狀況。單例實例初始化時和實例讀寫時的線程安全性。
先考慮第一種狀況。因爲在swift是在全局範圍內初始化變量。因此這樣的狀況較爲簡單。在Swift中。當全局變量被首次訪問調用時便被初始化,並且整個初始化過程具備原子操做性。由此。代碼的初始化過程便成爲一個臨界區並且在其它線程訪問調用全局變量前完畢初始化。Swift到底是怎麼作到的?事實上在整個過程當中。Swift經過dispatch_once函數使用了GCD。
若想了解得不少其它的話請看這篇Swift官方Blog。
在線程安全的模式下dispatch_once僅僅會運行閉包一次。
當一個在臨界區運行的線程--向dispatch_once傳入一個任務--在它結束運行前其餘的線程都會被限制住。
一旦運行完畢,它和其餘線程便不會再次在此區域運行。經過let把單例定義爲全局定量的話。咱們就可以保證這個變量的值在初始化後不會被改動。
總之,Swift聲明的所有全局定量都是經過線程安全的初始化獲得的單例。
但咱們仍是要考慮讀寫問題。雖然Swift經過使用dispatch_once確保咱們在線程安全的模式下初始化單例,但這並不能表明單例的數據類型相同具備線程安全性。舉個樣例,假如一個全局變量是一個類的實例,你仍可以在類內的臨界區操控內部數據,這將需要利用其它的方式來保證線程安全性。
處理讀取與寫入問題
保證線程安全性的實例化不是咱們處理單例時的惟一問題。
假如一個單例屬性表明着一個可變的對象。比方像PhotoManager 中的photos數組。那麼你就需要考慮那個對象是否就有線程安全性。
在Swift中不論什麼用let聲明的變量都是一個僅僅可讀併線程安全的常量。但是用var聲明的變量都是值可變且併線程不安全的。比方Swift中像Array和Dictionary這種集合類型若被聲明爲值可變的話,它們就是線程不安全的。
那Foundation中的NSArray線程是否安全呢?不必定!
蘋果還專門爲那些線程非安全的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的property:
1
2
3
4
|
private var _photos: [Photo] = []
var photos: [Photo] {
return _photos
}
|
當property的getter讀取可變數組的時候它就是一個讀取函數。調用者獲得一份數組的copy並阻止原數組被不當改動,但這不能在一個線程調用addPhoto方法的同一時候阻止還有一個線程回調photo的property的getter。
提醒:在上述代碼中,調用者爲何不直接獲得一份photos的copy呢?這是因爲在Swift中,所有的參數和函數的返回值都是經過猜測(Reference)或值傳輸的。經過猜測進行傳輸和Objective-C中傳輸指針是同樣的,這就表明着你可以訪問調用原始對象。並且對於同一對象的猜測後其不論什麼改變都可以被顯示出來。在對象的copy中經過值結果傳值且對於copy的更改都不正確原是對象形成影響。Swift默認以猜測機制或結構體的值來傳輸類的實例。
Swift中的Array和Dictionary都是經過結構體來實現的。當你向前或向後傳輸這些實例的時候,你的代碼將會運行很是屢次的copy。這時不要小心內存使用問題。因爲這些Swift的集合類型(如Array、Dictionary)的運行過程都已被優化。僅僅有在必要的時候纔會進行copy。
對於來一個經過值傳輸的Array實例來講,僅僅有在被傳輸後纔會進行其第一次改動。
這是一個常見的軟件開發環境下的讀寫問題。GCD經過使用dispatch barriers提供了一個具備讀/寫鎖的完美解決方式。
在使用併發隊列時,dispatch barriers即是一組像連續性路障的函數。使用GCD的barrier API保證了被傳輸的閉包是在特定時間內、在特定隊列上運行的惟一任務。這就意味着在派發的barrier前傳輸的任務必須在特定閉包開始運行前完畢運行。
當閉包到達後,barrier便開始運行閉包並保證此段時間內隊列不會再運行不論什麼其它的閉包。特定閉包一旦完畢運行,隊列便會返回其默認的運行狀態。
GCD相同提供了具備同步與異步功能的barrier函數。
如下的圖式描寫敘述了在多個異步任務中的barrier函數的執行效果:
dispatch barrier
需要注意的是在barrier執行前程序是以併發隊列的形式執行,但當barrier一旦開始執行後,程序便以連續隊列的形式執行。沒錯,barrier是這段特定時間內惟一被執行的任務。當barrier執行結束後,程序再次回到了普通的併發隊列執行狀態。
對於barrier函數咱們作一些必要的說明:
本身定義連續隊列(Custom Serial Queue):在這樣的狀況下不是特別建議使用barrier。因爲barrier在連續隊列運行期間不會起到不論什麼幫助。
全局併發隊列(Global Concurrent Queue):慎重使用;當其它系統也在使用隊列的時候。你應該不想把所有的隊列都壟爲本身所用。
本身定義併發隊列(Custom Concurrent Queue):適用於涉及臨界區及原子性的代碼。在不論什麼你想要保正設定(setting)或初始化具備線程安全性的狀況下。barrier都是一個不錯的選擇。
從上面對於本身定義併發序列解釋可以得出結論。你得寫一個本身的barrier函數並將讀取函數和寫入函數彼此分開。併發序列將贊成多個讀取過程同步執行。
打開PhotoManager.swift,在photos屬性下給類文件加入例如如下的私有屬性:
1
2
|
private let concurrentPhotoQueue = dispatch_queue_create(
"com.raywenderlich.GooglyPuff.photoQueue" , DISPATCH_QUEUE_CONCURRENT)
|
經過dispatch_queue_create函數初始化了一個名爲concurrentPhotoQueue的併發隊列。
第一個參數是一個逆DNS風格的命名方式。其描寫敘述在debugging時會很實用。
第二個參數設定了你的隊列是連續性的仍是併發性的。
很是多網上的實例代碼中都喜歡給dispatch_queue_create的第二個參數設定爲0或NULL。事實上這是一種過期的聲明連續分派隊列的方法。你最好用你本身的參數設定它。
找到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()
}
}
}
|
你的新函數是這樣工做的:
經過使用你本身的本身定義隊列加入寫入過程,在不久後臨界區運行的時候這將是你的隊列中惟一運行的任務。
向數組中加入對象。僅僅要這是一個barrier屬性的閉包。那麼它在concurrentPhotoQueue隊列中毫不會和其它閉包同一時候執行。
最後你推送了一個照片加入完成的消息。這個消息應該從主線程推送因爲它將處理一些涉及UI的工做。因此你爲這個消息以異步的形式向主線程派發了任務。
以上便處理好了寫入方法的問題,但是你還要處理一下photos的讀取方法。
爲了保證寫入方面的線程安全行,你需要在concurrentPhotoQueue隊列中執行讀取方法。
因爲你需要從函數獲取返回值並且在讀取任務返回前不會執行不論什麼其它的任務,因此你不能向隊列異步派發任務。
在這樣的狀況下,dispatch_sync是一個不錯的選擇。
dispatch_sync可以同步傳輸任務並在其返回前等待其完畢。使用dispatch_sync跟蹤含有派發barrier的任務,或者在當你需要使用閉包中的數據時而要等待執行結束的時候使用dispatch_sync。
慎重也是必要的。想象一下,當你對一個當即要執行的隊列調用dispatch_sync時,這將形成死鎖。因爲調用要等到閉包B執行後才幹開始執行,但是這個閉包B僅僅有等到當前執行的且不可能結束的閉包A執行結束後纔有可能結束。
這將迫使你時刻注意本身調用的的或是傳入的隊列。
來看一下dispatch_sync的使用說明:
本身定義連續隊列(Custome Serial Queue):這樣的狀況下必定要很當心;假如一個隊列中正在運行任務並且你將這個隊列傳入dispatch_sync中使用,這毫無疑問會形成死鎖。
主隊列(Main Queue[Serial]):相同需要當心發生死鎖。
併發隊列(Concurrent Queue):在對派發barrier運行同步工做或等待一個任務的運行結束後需要進行下一步處理的狀況下,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
}
|
分佈解釋一下:
同步派發concurrentPhotoQueue使其運行讀取功能。
儲存照片數組至photosCopy並返回。
恭喜--你的PhotoManager單好比今線程安全了。不管現在是執行讀取仍是寫入功能。你都可以保證整個單例在安全模式下執行。
隊列可視化
還不能全然理解GCD的基礎知識?接下來咱們將在一個簡單的演示樣例中使用斷點和NSLog功能確保你進一步理解GCD函數執行原理。
我將使用兩個動態的GIF幫助你理解dispatch_async和dispatch_sync。在GIF的每步切換下,注意代碼斷點與圖式的關係。
dispatch_sync重覽
1
2
3
4
5
6
7
8
|
override func viewDidLoad() {
super .viewDidLoad()
dispatch_sync(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
NSLog( "First Log" )
}
NSLog( "Second Log" )
}
|
dispatch_sync
分佈解釋:
主隊列按順序運行任務,下一個將要被運行的任務即是實例化包括viewDidLoad的UIViewController。
主隊列開始運行viewDidLoad。
dispatch_sync閉包加入至全局隊列並在稍後被運行。在此閉包完畢運行前主隊列上的工做將被暫停。
回調的閉包可以被併發運行並以FIFO的順序加入至一個全局隊列。
這個全局隊列還包括加入dispatch_sync閉包前的多個任務。
最終輪到dispatch_sync閉包運行了。
閉包運行結束後主隊列開始恢復工做。
viewDidLoad函數運行結束。主隊列開始處理其它任務。
dispatch_sync函數向隊列加入了一個任務並等待任務完畢。
事實上dispatch_async也差點兒相同。僅僅只是它不會等待任務完畢便會返回線程。
dispatch_async重覽
1
2
3
4
5
6
7
8
|
override func viewDidLoad() {
super .viewDidLoad()
dispatch_async(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
NSLog( "First Log" )
}
NSLog( "Second Log" )
}
|
dispatch_async
主隊列按順序運行任務,下一個將要被運行的任務即是實例化包括viewDidLoad的`UIViewControl。
主隊列開始運行viewDidLoad。
dispatch_async閉包加入至全局隊列並在稍後被運行。
向全局隊列加入dispatch_async閉包後viewDidLoad函數繼續執行。主線程繼續其剩餘的任務。
與此同一時候全局隊列是併發性的處理它的任務的。可被併發執行的閉包將以FIFO的順序加入至全局隊列。
經過dispatch_async加入的閉包開始運行。
dispatch_async閉包運行結束,並且所有的NSLog語句都已被顯示在控制檯上。
在這個樣例中,第二個NSLog語句運行後第一個NSLog語句才運行。這樣的狀況並不是每次都會發生的--這取決於硬件在給定的時間內所處理的工做,並且你對於哪一個語句會先被運行一無所知且毫無控制權。
沒準「第一個」NSLog就會做爲第一個log出現。
Where to Go From Here?
在這篇教程中,你學會了怎樣讓你的代碼具備線程安全性和怎樣在CPU高密度處理多個任務的時候獲取主線程的響應。
你可以從這裏下載GooglyPuff的完整代碼,在下一節教程中你將會繼續在這個project中進行改動。
假如你打算優化你的App,我認爲你真的該使用Instruments中的Time Profile. 詳細教程請查看這篇How To Use Instruments。