原文 Grand Central Dispatch Tutorail for Swift: Part 1/2
原文作者:Bjrn Olav Ruud
譯者:Ethan Joe
盡管Grand Central Dispatch(以下簡稱為GCD)已推出一段時間了,但並不是所有人都明白其原理;當然這是可以理解的,畢竟程序的並發機制很繁瑣,而且基於C的GCD的API對於Swift的新世界並不是特別友好。
在接下來的兩節教程中,你將學習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才開始執行,但任務1、2、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中選擇照片,也可以通過網絡從事先設置好的地址下載照片。
GooglyPuff Swift Start 1
將工程下載至本地後用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函數替換成下述代碼:
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變量下面添加一些全局的簡潔化變量,以此使得調用全局隊列更加簡便。
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) } }
這樣就使得派發隊列的調用的代碼更加具有可讀性並很輕松地得知哪個隊列正在被使用。
利用dispatch_after實現延遲
考慮一下你App的UX。你的App有沒有使得用戶在第一次打開App的時候不知道該干些什麼而感到不知所措呢?: ]
假如在PhotoManager中沒有任何一張照片的時候便向用戶發出提醒應該是一個不錯的主意。不管怎樣,你還是要考慮一下用戶在App主頁面上的注意力:假如你的提醒顯示得過快的話,用戶沒准在因為看著其他地方而錯過它。
當用戶第一次使用App的時候,在提醒顯示前執行一秒鐘的延遲應該足以吸引住用戶的注意力。
在PhotoCollectionViewController.swift底部的showOrHideBarPrompt函數中添加如下代碼:
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函數中找一找錯誤:
func addPhoto(photo: Photo) { _photos.append(photo) dispatch_async(dispatch_get_main_queue()) { self.postContentAddedNotification() } }
這個寫取方法修改了可變數組的對象。
再來看一看photos的property:
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屬性下給類文件添加如下的私有屬性:
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函數並代替為以下代碼:
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屬性:
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重覽
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重覽
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的東西。