原文 Grand Central Dispatch Tutorail for Swift: Part 1/2 html
原文做者:Bjrn Olav Ruud ios
譯者:Ethan Joe swift
儘管Grand Central Dispatch(如下簡稱爲GCD)已推出一段時間了,但並非全部人都明白其原理;固然這是能夠理解的,畢竟程序的併發機制很繁瑣,並且基於C的GCD的API對於Swift的新世界並非特別友好。 數組
在接下來的兩節教程中,你將學習GCD的輸入 (in)與輸出 (out)。第一節將解釋什麼是GCD並瞭解幾個GCD的基礎函數。在第二節,你將學習幾個更加進階的GCD函數。 xcode
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中選擇照片,也能夠經過網絡從事先設置好的地址下載照片。
將工程下載至本地後用Xcode打開並編譯運行。它看起來是這樣的:
GooglyPuff
在工程中共有四個類文件:
PhotoCollectionViewController:這是App運行後顯示的首個界面。它將顯示全部被選照片的縮略圖。
PhotoDetailViewController:它將處理將咕嚕眼添加至照片的工做並將處理完畢的照片顯示在UIScrollView中。
Photo:一個包含着照片基本屬性的協議,其中有image(未處理照片)、thumbnail(裁減後的照片)及status(照片能否使用狀態);兩個用來實現協議的類,DownloadPhoto將從一個NSURL實例中實例化照片,而AssetPhoto則從一個ALAsset實例中實例化照片。
PhotoManager:這個類將管理全部Photo類型對象。
使用dispatch_async處理後臺任務
回到剛纔運行的App後,經過本身的Photo Library添加照片或是使用Le internet下載一些照片。
須要注意的是當你點擊PhotoCollectionViewController中的一個UICollectionViewCell後,界面切換至一個新的PhotoDetailViewController所用的時間;對於那些處理速度較慢的設備來講,處理一張較大的照片會產生一個很是明顯的延遲。
這種狀況下很容易使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的完整代碼,在下一節教程中你將會繼續在這個工程中進行修改。
假如你打算優化你的App,我以爲你真的該使用Instruments中的Time Profile. 具體教程請查看這篇How To Use Instruments。
在下一節教程中,你將會利用更深層次的GCD的API去作些更Cool的東西。