GCD的深入理解
GCD 深入理解(一)
本文由@nixzhu翻譯至raywenderlich的《grand-central-dispatch-in-depth-part-1》
雖然 GCD 已經出現過一段時間了,但不是每個人都明了其主要內容。這是可以理解的;并發一直很棘手,而 GCD 是基于 C 的 API ,它們就像一組尖銳的棱角戳進 Objective-C 的平滑世界。我們將分兩個部分的教程來深入學習 GCD 。
在這兩部分的系列中,第一個部分的將解釋 GCD 是做什么的,并從許多基本的 GCD 函數中找出幾個來展示。在第二部分,你將學到幾個 GCD 提供的高級函數。
什么是 GCD
GCD 是 libdispatch 的市場名稱,而 libdispatch 作為 Apple 的一個庫,為并發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具有以下優點:
1.GCD 能通過推遲昂貴計算任務并在后臺運行它們來改善你的應用的響應性能。
2.GCD 提供一個易于使用的并發模型而不僅僅只是鎖和線程,以幫助我們避開并發陷阱。
3.GCD 具有在常見模式(例如單例)上用更高性能的原語優化你的代碼的潛在能力。
本教程假設你對 Block 和 GCD 有基礎了解。如果你對 GCD 完全陌生,先看看??iOS 上的多線程和 GCD 入門教程?學習其要領。?GCD 術語
要理解 GCD ,你要先熟悉與線程和并發相關的幾個概念。這兩者都可能模糊和微妙,所以在開始 GCD 之前先簡要地回顧一下它們。
Serial vs. Concurrent 串行 vs. 并發
這些術語描述當任務相對于其它任務被執行,任務串行執行就是每次只有一個任務被執行,任務并發執行就是在同一時間可以有多個任務被執行。
雖然這些術語被廣泛使用,本教程中你可以將任務設定為一個 Objective-C 的 Block 。不明白什么是 Block ?看看??iOS 5 教程中的如何使用 Block?。實際上,你也可以在 GCD 上使用函數指針,但在大多數場景中,這實際上更難于使用。Block 就是更加容易些!?Synchronous vs. Asynchronous 同步 vs. 異步
在 GCD 中,這些術語描述當一個函數相對于另一個任務完成,此任務是該函數要求 GCD 執行的。一個同步函數只在完成了它預定的任務后才返回。
一個異步函數,剛好相反,會立即返回,預定的任務會完成但不會等它完成。因此,一個異步函數不會阻塞當前線程去執行下一個函數。
注意——當你讀到同步函數“阻塞(Block)”當前線程,或函數是一個“阻塞”函數或阻塞操作時,不要被搞糊涂了!動詞“阻塞”描述了函數如何影響它所在的線程而與名詞“代碼塊(Block)”沒有關系。代碼塊描述了用 Objective-C 編寫的一個匿名函數,它能定義一個任務并被提交到 GCD 。
譯者注:中文不會有這個問題,“阻塞”和“代碼塊”是兩個詞。
Critical Section 臨界區
就是一段代碼不能被并發執行,也就是,兩個線程不能同時執行這段代碼。這很常見,因為代碼去操作一個共享資源,例如一個變量若能被并發進程訪問,那么它很可能會變質(譯者注:它的值不再可信)。
Race Condition 競態條件
這種狀況是指基于特定序列或時機的事件的軟件系統以不受控制的方式運行的行為,例如程序的并發任務執行的確切順序。競態條件可導致無法預測的行為,而不能通過代碼檢查立即發現。
Deadlock 死鎖
兩個(有時更多)東西——在大多數情況下,是線程——所謂的死鎖是指它們都卡住了,并等待對方完成或執行其它操作。第一個不能完成是因為它在等待第二個的完成。但第二個也不能完成,因為它在等待第一個的完成。
Thread Safe 線程安全
線程安全的代碼能在多線程或并發任務中被安全的調用,而不會導致任何問題(數據損壞,崩潰,等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary 。你可以在同一時間在多個線程中使用它而不會有問題。另一方面,NSMutableDictionary 就不是線程安全的,應該保證一次只能有一個線程訪問它。
Context Switch 上下文切換
一個上下文切換指當你在單個進程里切換執行不同的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很普遍,但會帶來一些額外的開銷。
Concurrency vs Parallelism 并發與并行
并發和并行通常被一起提到,所以值得花些時間解釋它們之間的區別。
并發代碼的不同部分可以“同步”執行。然而,該怎樣發生或是否發生都取決于系統。多核設備通過并行來同時執行多個線程;然而,為了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,然后運行另一個線程或進程。這通常發生地足夠快以致給我們并發執行地錯覺,如下圖所示:?
雖然你可以編寫代碼在 GCD 下并發執行,但 GCD 會決定有多少并行的需求。并行要求并發,但并發并不能保證并行。
更深入的觀點是并發實際上是關于構造。當你在腦海中用 GCD 編寫代碼,你組織你的代碼來暴露能同時運行的多個工作片段,以及不能同時運行的那些。如果你想深入此主題,看看?this excellent talk by Rob Pike?。?Queues 隊列
GCD 提供有 dispatch queues 來處理代碼塊,這些隊列管理你提供給 GCD 的任務并用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列里的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。
所有的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程并行的訪問它們。 GCD 的優點是顯而易見的,即當你了解了調度隊列如何為你自己代碼的不同部分提供線程安全。關于這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工作。
在本節你會看到兩種調度隊列,都是由 GCD 提供的,然后看一些描述如何用調度函數添加工作到隊列的列子。
Serial Queues 串行隊列?
這些任務的執行時機受到 GCD 的控制;唯一能確保的事情是 GCD 一次只執行一個任務,并且按照我們添加到隊列的順序來執行。
由于在串行隊列中不會有兩個任務并發運行,因此不會出現同時訪問臨界區的風險;相對于這些任務來說,這就從競態條件下保護了臨界區。所以如果訪問臨界區的唯一方式是通過提交到調度隊列的任務,那么你就不需要擔心臨界區的安全問題了。
Concurrent Queues 并發隊列
在并發隊列中的任務能得到的保證是它們會按照被添加的順序開始執行,但這就是全部的保證了。任務可能以任意順序完成,你不會知道何時開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這完全取決于 GCD 。
下圖展示了一個示例任務執行計劃,GCD 管理著四個并發任務:?
注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始后,Block 1等待了好一會兒才開始。同樣, Block 3 在 Block 2 之后才開始,但它先于 Block 2 完成。
何時開始一個 Block 完全取決于 GCD 。如果一個 Block 的執行時間與另一個重疊,也是由 GCD 來決定是否將其運行在另一個不同的核心上,如果那個核心可用,否則就用上下文切換的方式來執行不同的 Block 。
有趣的是, GCD 提供給你至少五個特定的隊列,可根據隊列類型選擇使用。
Queue Types 隊列類型
首先,系統提供給你一個叫做 主隊列(main queue) 的特殊隊列。和其它串行隊列一樣,這個隊列中的任務一次只能執行一個。然而,它能保證所有的任務都在主線程執行,而主線程是唯一可用于更新 UI 的線程。這個隊列就是用于發生消息給 UIView 或發送通知的。
系統同時提供給你好幾個并發隊列。它們叫做 全局調度隊列(Global Dispatch Queues) 。目前的四個全局隊列有著不同的優先級:background、low、default 以及 high。要知道,Apple 的 API 也會使用這些隊列,所以你添加的任何任務都不會是這些隊列中唯一的任務。
最后,你也可以創建自己的串行隊列或并發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你自己創建的隊列。
以上是調度隊列的大框架!
GCD 的“藝術”歸結為選擇合適的隊列來調度函數以提交你的工作。體驗這一點的最好方式是走一遍下邊的列子,我們沿途會提供一些一般性的建議。
入門
既然本教程的目標是優化且安全的使用 GCD 調用來自不同線程的代碼,那么你將從一個近乎完成的叫做 GooglyPuff 的項目入手。
GooglyPuff 是一個沒有優化,線程不安全的應用,它使用 Core Image 的人臉檢測 API 來覆蓋一對曲棍球眼睛到被檢測到的人臉上。對于基本的圖像,可以從相機膠卷選擇,或用預設好的URL從互聯網下載。
點擊此處下載項目完成項目下載之后,將其解壓到某個方便的目錄,再用 Xcode 打開它并編譯運行。這個應用看起來如下圖所示:
注意當你選擇 Le Internet 選項下載圖片時,一個 UIAlertView 過早地彈出。你將在本系列教程地第二部分修復這個問題。
這個項目中有四個有趣的類:
1. PhotoCollectionViewController:它是應用開始的第一個視圖控制器。它用縮略圖展示所有選定的照片。
2. PhotoDetailViewController:它執行添加曲棍球眼睛到圖像上的邏輯,并用一個 UIScrollView 來顯示結果圖片。
3. Photo:這是一個類簇,它根據一個 NSURL 的實例或一個 ALAsset 的實例來實例化照片。這個類提供一個圖像、縮略圖以及從 URL 下載的狀態。
4. PhotoManager:它管理所有 Photo 的實例.
用 dispatch_async 處理后臺任務
回到應用并從你的相機膠卷添加一些照片或使用 Le Internet 選項下載一些。
注意在按下 PhotoCollectionViewController 中的一個 UICollectionViewCell 到生成一個新的 PhotoDetailViewController 之間花了多久時間;你會注意到一個明顯的滯后,特別是在比較慢的設備上查看很大的圖。
在重載 UIViewController 的 viewDidLoad 時容易加入太多雜波(too much clutter),這通常會引起視圖控制器出現前更長的等待。如果可能,最好是卸下一些工作放到后臺,如果它們不是絕對必須要運行在加載時間里。
這聽起來像是 dispatch_async 能做的事情!
打開 PhotoDetailViewController 并用下面的實現替換 viewDidLoad :
下面來說明上面的新代碼所做的事:
1. 你首先將工作從主線程移到全局線程。因為這是一個 dispatch_async() ,Block 會被異步地提交,意味著調用線程地執行將會繼續。這就使得 viewDidLoad 更早地在主線程完成,讓加載過程感覺起來更加快速。同時,一個人臉檢測過程會啟動并將在稍后完成。
2. 在這里,人臉檢測過程完成,并生成了一個新的圖像。既然你要使用此新圖像更新你的 UIImageView ,那么你就添加一個新的 Block 到主線程。記住——你必須總是在主線程訪問 UIKit 的類。
3. 最后,你用 fadeInNewImage: 更新 UI ,它執行一個淡入過程切換到新的曲棍球眼睛圖像。
編譯并運行你的應用;選擇一個圖像然后你會注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在之后就加上了。這給應用帶來了不錯的效果,和之前的顯示差別巨大。
進一步,如果你試著加載一個超大的圖像,應用不會在加載視圖控制器上“掛住”,這就使得應用具有很好伸縮性。
正如之前提到的, dispatch_async 添加一個 Block 都隊列就立即返回了。任務會在之后由 GCD 決定執行。當你需要在后臺執行一個基于網絡或 CPU 緊張的任務時就使用 dispatch_async ,這樣就不會阻塞當前線程。
下面是一個關于在 dispatch_async 上如何以及何時使用不同的隊列類型的快速指導:
1. 自定義串行隊列:當你想串行執行后臺任務并追蹤它時就是一個好選擇。這消除了資源爭用,因為你知道一次只有一個任務在執行。注意若你需要來自某個方法的數據,你必須內聯另一個 Block 來找回它或考慮使用 dispatch_sync。
2. 主隊列(串行):這是在一個并發隊列上完成任務后更新 UI 的共同選擇。要這樣做,你將在一個 Block 內部編寫另一個 Block 。以及,如果你在主隊列調用 dispatch_async 到主隊列,你能確保這個新任務將在當前方法完成后的某個時間執行。
3. 并發隊列:這是在后臺執行非 UI 工作的共同選擇。
使用 dispatch_after 延后工作
稍微考慮一下應用的 UX 。是否用戶第一次打開應用時會困惑于不知道做什么?你是這樣嗎? :]
如果用戶的 PhotoManager 里還沒有任何照片,那么顯示一個提示會是個好主意!然而,你同樣要考慮用戶的眼睛會如何在主屏幕上瀏覽:如果你太快的顯示一個提示,他們的眼睛還徘徊在視圖的其它部分上,他們很可能會錯過它。
顯示提示之前延遲一秒鐘就足夠捕捉到用戶的注意,他們此時已經第一次看過了應用。
添加如下代碼到到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的廢止實現里:
showOrHideNavPrompt 在 viewDidLoad 中執行,以及 UICollectionView 被重新加載的任何時候。按照注釋數字順序看看:
1. 你聲明了一個變量指定要延遲的時長。
2. 然后等待 delayInSeconds 給定的時長,再異步地添加一個 Block 到主線程。
編譯并運行應用。應該有一個輕微地延遲,這有助于抓住用戶的注意力并展示所要做的事情。
dispatch_after 工作起來就像一個延遲版的 dispatch_async 。你依然不能控制實際的執行時間,且一旦 dispatch_after 返回也就不能再取消它。
不知道何時適合使用 dispatch_after ?
1. 自定義串行隊列:在一個自定義串行隊列上使用 dispatch_after 要小心。你最好堅持使用主隊列。
2. 主隊列(串行):是使用 dispatch_after 的好選擇;Xcode 提供了一個不錯的自動完成模版。
3. 并發隊列:在并發隊列上使用 dispatch_after 也要小心;你會這樣做就比較罕見。還是在主隊列做這些操作吧。
讓你的單例線程安全
單例,不論喜歡還是討厭,它們在 iOS 上的流行情況就像網上的貓。 :]
一個常見的擔憂是它們常常不是線程安全的。這個擔憂十分合理,基于它們的用途:單例常常被多個控制器同時訪問。
單例的線程擔憂范圍從初始化開始,到信息的讀和寫。PhotoManager 類被實現為單例——它在目前的狀態下就會被這些問題所困擾。要看看事情如何很快地失去控制,你將在單例實例上創建一個控制好的競態條件。
導航到 PhotoManager.m 并找到 sharedManager ;它看起來如下:
當前狀態下,代碼相當簡單;你創建了一個單例并初始化一個叫做 photosArray 的 NSMutableArray 屬性。
然而,if 條件分支不是?線程安全的;如果你多次調用這個方法,有一個可能性是在某個線程(就叫它線程A)上進入 if 語句塊并可能在 sharedPhotoManager 被分配內存前發生一個上下文切換。然后另一個線程(線程B)可能進入 if ,分配單例實例的內存,然后退出。?當系統上下文切換回線程A,你會分配另外一個單例實例的內存,然后退出。在那個時間點,你有了兩個單例的實例——很明顯這不是你想要的(譯者注:這還能叫單例嗎?)!
要強制這個(競態)條件發生,替換 PhotoManager.m 中的 sharedManager 為下面的實現:
上面的代碼中你用 NSThread 的 sleepForTimeInterval: 類方法來強制發生一個上下文切換。
打開 AppDelegate.m 并添加如下代碼到 application:didFinishLaunchingWithOptions: 的最開始處:
這里創建了多個異步并發調用來實例化單例,然后引發上面描述的競態條件。
編譯并運行項目;查看控制臺輸出,你會看到多個單例被實例化,如下所示:?
注意到這里有好幾行顯示著不同地址的單例實例。這明顯違背了單例的目的,對吧?
這個輸出向你展示了臨界區被執行多次,而它只應該執行一次?,F在,固然是你自己強制這樣的狀況發生,但你可以想像一下這個狀況會怎樣在無意間發生。
注意:基于其它你無法控制的系統事件,NSLog 的數量有時會顯示多個。線程問題極其難以調試,因為它們往往難以重現。
要糾正這個狀況,實例化代碼應該只執行一次,并阻塞其它實例在 if 條件的臨界區運行。這剛好就是 dispatch_once 能做的事。
在單例初始化方法中用 dispatch_once 取代 if 條件判斷,如下所示:
編譯并運行你的應用;查看控制臺輸出,你會看到有且僅有一個單例的實例——這就是你對單例的期望!:]
現在你已經明白了防止競態條件的重要性,從 AppDelegate.m 中移除 dispatch_async 語句,并用下面的實現替換 PhotoManager 單例的初始化:
dispatch_once() 以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once 的代碼)的不同的線程會在臨界區已有一個線程的情況下被阻塞,直到臨界區完成為止。?
需要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類本身線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的情況。這些需要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。
處理讀者與寫者問題
線程安全實例不是處理單例時的唯一問題。如果單例屬性表示一個可變對象,那么你就需要考慮是否那個對象自身線程安全。
如果問題中的這個對象是一個 Foundation 容器類,那么答案是——“很可能不安全”!Apple 維護一個?有用且有些心寒的列表,眾多的 Foundation 類都不是線程安全的。 NSMutableArray,已用于你的單例,正在那個列表里休息。?雖然許多線程可以同時讀取 NSMutableArray 的一個實例而不會產生問題,但當一個線程正在讀取時讓另外一個線程修改數組就是不安全的。你的單例在目前的狀況下不能預防這種情況的發生。
要分析這個問題,看看 PhotoManager.m 中的 addPhoto:,轉載如下:
這是一個寫方法,它修改一個私有可變數組對象。
現在看看 photos ,轉載如下:?
這是所謂的讀方法,它讀取可變數組。它為調用者生成一個不可變的拷貝,防止調用者不當地改變數組,但這不能提供任何保護來對抗當一個線程調用讀方法 photos 的同時另一個線程調用寫方法 addPhoto: 。
這就是軟件開發中經典的讀者寫者問題。GCD 通過用 dispatch barriers 創建一個讀者寫者鎖 提供了一個優雅的解決方案。
Dispatch barriers 是一組函數,在并發隊列上工作時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上唯一被執行的條目。這就意味著所有的先于調度障礙提交到隊列的條目必能在這個 Block 執行前完成。
當這個 Block 的時機到達,調度障礙執行這個 Block 并確保在那個時間里隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。
下圖顯示了障礙函數對多個異步隊列的影響:?
注意到正常部分的操作就如同一個正常的并發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是唯一在執行的事物。在障礙完成后,隊列回到一個正常并發隊列的樣子。
下面是你何時會——和不會——使用障礙函數的情況:
1. 自定義串行隊列:一個很壞的選擇;障礙不會有任何幫助,因為不管怎樣,一個串行隊列一次都只執行一個操作。
2. 全局并發隊列:要小心;這可能不是最好的主意,因為其它系統可能在使用隊列而且你不能壟斷它們只為你自己的目的。
3. 自定義并發隊列:這對于原子或臨界區代碼來說是極佳的選擇。任何你在設置或實例化的需要線程安全的事物都是使用障礙的最佳候選。
由于上面唯一像樣的選擇是自定義并發隊列,你將創建一個你自己的隊列去處理你的障礙函數并分開讀和寫函數。且這個并發隊列將允許多個多操作同時進行。
打開 PhotoManager.m,添加如下私有屬性到類擴展中:
找到 addPhoto: 并用下面的實現替換它:
你新寫的函數是這樣工作的:
1. 在執行下面所有的工作前檢查是否有合法的相片。
2. 添加寫操作到你的自定義隊列。當臨界區在稍后執行時,這將是你隊列中唯一執行的條目。
3. 這是添加對象到數組的實際代碼。由于它是一個障礙 Block ,這個 Block 永遠不會同時和其它 Block 一起在 concurrentPhotoQueue 中執行。
4. 最后你發送一個通知說明完成了添加圖片。這個通知將在主線程被發送因為它將會做一些 UI 工作,所以在此為了通知,你異步地調度另一個任務到主線程。
這就處理了寫操作,但你還需要實現 photos 讀方法并實例化 concurrentPhotoQueue 。
在寫者打擾的情況下,要確保線程安全,你需要在 concurrentPhotoQueue 隊列上執行讀操作。既然你需要從函數返回,你就不能異步調度到隊列,因為那樣在讀者函數返回之前不一定運行。
在這種情況下,dispatch_sync 就是一個絕好的候選。
dispatch_sync() 同步地提交工作并在返回前等待它完成。使用 dispatch_sync 跟蹤你的調度障礙工作,或者當你需要等待操作完成后才能使用 Block 處理過的數據。如果你使用第二種情況做事,你將不時看到一個 __block 變量寫在 dispatch_sync 范圍之外,以便返回時在 dispatch_sync 使用處理過的對象。
但你需要很小心。想像如果你調用 dispatch_sync 并放在你已運行著的當前隊列。這會導致死鎖,因為調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務無法完成!這將迫使你自覺于你正從哪個隊列調用——以及你正在傳遞進入哪個隊列。
下面是一個快速總覽,關于在何時以及何處使用 dispatch_sync :
1. 自定義串行隊列:在這個狀況下要非常小心!如果你正運行在一個隊列并調用 dispatch_sync 放在同一個隊列,那你就百分百地創建了一個死鎖。
2. 主隊列(串行):同上面的理由一樣,必須非常小心!這個狀況同樣有潛在的導致死鎖的情況。
3. 并發隊列:這才是做同步工作的好選擇,不論是通過調度障礙,或者需要等待一個任務完成才能執行進一步處理的情況。
繼續在 PhotoManager.m 上工作,用下面的實現替換 photos :
這就是你的讀函數。按順序看看編過號的注釋,有這些:
1. __block 關鍵字允許對象在 Block 內可變。沒有它,array 在 Block 內部就只是只讀的,你的代碼甚至不能通過編譯。
2. 在 concurrentPhotoQueue 上同步調度來執行讀操作。
3. 將相片數組存儲在 array 內并返回它。
最后,你需要實例化你的 concurrentPhotoQueue 屬性。修改 sharedManager 以便像下面這樣初始化隊列:
這里使用 dispatch_queue_create 初始化 concurrentPhotoQueue 為一個并發隊列。第一個參數是反向DNS樣式命名慣例;確保它是描述性的,將有助于調試。第二個參數指定你的隊列是串行還是并發。
注意:當你在網上搜索例子時,你會經常看人們傳遞 0 或者 NULL 給 dispatch_queue_create 的第二個參數。這是一個創建串行隊列的過時方式;明確你的參數總是更好。
恭喜——你的 PhotoManager 單例現在是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。
A Visual Review of Queueing 隊列的虛擬回顧
依然沒有 100% 地掌握 GCD 的要領?確保你可以使用 GCD 函數輕松地創建簡單的例子,使用斷點和 NSLog 語句保證自己明白當下發生的情況。
我在下面提供了兩個 GIF動畫來幫助你鞏固對 dispatch_async 和 dispatch_sync 的理解。包含在每個 GIF 中的代碼可以提供視覺輔助;仔細注意 GIF 左邊顯示代碼斷點的每一步,以及右邊相關隊列的狀態。
dispatch_sync 回顧
下面是圖中幾個步驟的說明:
1. 主隊列一路按順序執行任務——接著是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
2. viewDidLoad 在主線程執行。
3. 主線程目前在 viewDidLoad 內,正要到達 dispatch_sync 。
4. dispatch_sync Block 被添加到一個全局隊列中,將在稍后執行。進程將在主線程掛起直到該 Block 完成。同時,全局隊列并發處理任務;要記得 Block 在全局隊列中將按照 FIFO 順序出列,但可以并發執行。
5. 全局隊列處理 dispatch_sync Block 加入之前已經出現在隊列中的任務。
6. 終于,輪到 dispatch_sync Block 。
7. 這個 Block 完成,因此主線程上的任務可以恢復。
8. viewDidLoad 方法完成,主隊列繼續處理其他任務。
dispatch_sync 添加任務到一個隊列并等待直到任務完成。dispatch_async 做類似的事情,但不同之處是它不會等待任務的完成,而是立即繼續“調用線程”的其它任務。
dispatch_async 回顧
1.主隊列一路按順序執行任務——接著是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
2. viewDidLoad 在主線程執行。
3.主線程目前在 viewDidLoad 內,正要到達 dispatch_async 。
4.dispatch_async Block 被添加到一個全局隊列中,將在稍后執行。
5.viewDidLoad 在添加 dispatch_async 到全局隊列后繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列并發地處理它未完成地任務。記住 Block 在全局隊列中將按照 FIFO 順序出列,但可以并發執行。
6.添加到 dispatch_async 的代碼塊開始執行。
7.dispatch_async Block 完成,兩個 NSLog 語句將它們的輸出放在控制臺上。
在這個特定的實例中,第二個 NSLog 語句執行,跟著是第一個 NSLog 語句。并不總是這樣——著取決于給定時刻硬件正在做的事情,而且你無法控制或知曉哪個語句會先執行?!暗谝粋€” NSLog 在某些調用情況下會第一個執行。
下一步怎么走?
在本教程中,你學習了如何讓你的代碼線程安全,以及在執行 CPU 密集型任務時如何保持主線程的響應性。
你可以下載??GooglyPuff 項目,它包含了目前所有本教程中編寫的實現。在本教程的第二部分,你將繼續改進這個項目。? 如果你計劃優化你自己的應用,那你應該用 Instruments 中的 Time Profile 模版分析你的工作。對這個工具的使用超出了本教程的范圍,你可以看看??如何使用Instruments?來得到一個很好的概述。?同時請確保在真實設備上分析,而在模擬器上測試會對程序速度產生非常不準確的印象。
GCD 深入理解(二)
轉自@nixzhu的GitHub主頁(譯者:Riven、@nixzhu),原文《Grand Central Dispatch In-Depth: Part 2/2》
歡迎來到GCD深入理解系列教程的第二部分(也是最后一部分)。
在本系列的第一部分中,你已經學到超過你想像的關于并發、線程以及GCD 如何工作的知識。通過在初始化時利用 dispatch_once,你創建了一個線程安全的 PhotoManager 單例,而且你通過使用 dispatch_barrier_async 和 dispatch_sync 的組合使得對 Photos 數組的讀取和寫入都變得線程安全了。
除了上面這些,你還通過利用 dispatch_after 來延遲顯示提示信息,以及利用 dispatch_async 將 CPU 密集型任務從 ViewController 的初始化過程中剝離出來異步執行,達到了增強應用的用戶體驗的目的。
如果你一直跟著第一部分的教程在寫代碼,那你可以繼續你的工程。但如果你沒有完成第一部分的工作,或者不想重用你的工程,你可以下載第一部分最終的代碼。
那就讓我們來更深入地探索 GCD 吧!
糾正過早彈出的提示
你可能已經注意到當你嘗試用 Le Internet 選項來添加圖片時,一個 UIAlertView 會在圖片下載完成之前就彈出,如下如所示:?
問題的癥結在 PhotoManagers 的 downloadPhotoWithCompletionBlock: 里,它目前的實現如下:
在方法的最后你調用了 completionBlock ——因為此時你假設所有的照片都已下載完成。但很不幸,此時并不能保證所有的下載都已完成。
Photo 類的實例方法用某個 URL 開始下載某個文件并立即返回,但此時下載并未完成。換句話說,當 downloadPhotoWithCompletionBlock: 在其末尾調用 completionBlock 時,它就假設了它自己所使用的方法全都是同步的,而且每個方法都完成了它們的工作。
然而,-[Photo initWithURL:withCompletionBlock:] 是異步執行的,會立即返回——所以這種方式行不通。
因此,只有在所有的圖像下載任務都調用了它們自己的 Completion Block 之后,downloadPhotoWithCompletionBlock: 才能調用它自己的 completionBlock 。問題是:你該如何監控并發的異步事件?你不知道它們何時完成,而且它們完成的順序完全是不確定的。
或許你可以寫一些比較 Hacky 的代碼,用多個布爾值來記錄每個下載的完成情況,但這樣做就缺失了擴展性,而且說實話,代碼會很難看。
幸運的是, 解決這種對多個異步任務的完成進行監控的問題,恰好就是設計 dispatch_group 的目的。
Dispatch Groups(調度組)
Dispatch Group 會在整個組的任務都完成時通知你。這些任務可以是同步的,也可以是異步的,即便在不同的隊列也行。而且在整個組的任務都完成時,Dispatch Group 可以用同步的或者異步的方式通知你。因為要監控的任務在不同隊列,那就用一個 dispatch_group_t 的實例來記下這些不同的任務。
當組中所有的事件都完成時,GCD 的 API 提供了兩種通知方式。
第一種是 dispatch_group_wait ,它會阻塞當前線程,直到組里面所有的任務都完成或者等到某個超時發生。這恰好是你目前所需要的。
打開 PhotoManager.m,用下列實現替換 downloadPhotosWithCompletionBlock:
按照注釋的順序,你會看到:
1. 因為你在使用的是同步的 dispatch_group_wait ,它會阻塞當前線程,所以你要用 dispatch_async 將整個方法放入后臺隊列以避免阻塞主線程。
2. 創建一個新的 Dispatch Group,它的作用就像一個用于未完成任務的計數器。
3. dispatch_group_enter 手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對出現,否則你可能會遇到詭異的崩潰問題。
4. 手動通知 Group 它的工作已經完成。再次說明,你必須要確保進入 Group 的次數和離開 Group 的次數相等。
5. dispatch_group_wait 會一直等待,直到任務全部完成或者超時。如果在所有任務完成前超時了,該函數會返回一個非零值。你可以對此返回值做條件判斷以確定是否超出等待周期;然而,你在這里用 DISPATCH_TIME_FOREVER 讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,因為圖片的創建工作總是會完成的。
6. 此時此刻,你已經確保了,要么所有的圖片任務都已完成,要么發生了超時。然后,你在主線程上運行 completionBlock 回調。這會將工作放到主線程上,并在稍后執行。
7. 最后,檢查 completionBlock 是否為 nil,如果不是,那就運行它。
編譯并運行你的應用,嘗試下載多個圖片,觀察你的應用是在何時運行 completionBlock 的。?
| 注意:如果你是在真機上運行應用,而且網絡活動發生得太快以致難以觀察 completionBlock 被調用的時刻,那么你可以在 Settings 應用里的開發者相關部分里打開一些網絡設置,以確保代碼按照我們所期望的那樣工作。只需去往 Network Link Conditioner 區,開啟它,再選擇一個 Profile,“Very Bad Network” 就不錯。 |
如果你是在模擬器里運行應用,你可以使用 來自?GitHub 的 Network Link Conditioner?來改變網絡速度。它會成為你工具箱中的一個好工具,因為它強制你研究你的應用在連接速度并非最佳的情況下會變成什么樣。
目前為止的解決方案還不錯,但是總體來說,如果可能,最好還是要避免阻塞線程。你的下一個任務是重寫一些方法,以便當所有下載任務完成時能異步通知你。
在我們轉向另外一種使用 Dispatch Group 的方式之前,先看一個簡要的概述,關于何時以及怎樣使用有著不同的隊列類型的 Dispatch Group :
1. 自定義串行隊列:它很適合當一組任務完成時發出通知。
2. 主隊列(串行):它也很適合這樣的情況。但如果你要同步地等待所有工作地完成,那你就不應該使用它,因為你不能阻塞主線程。然而,異步模型是一個很有吸引力的能用于在幾個較長任務(例如網絡調用)完成后更新 UI 的方式。
3. 并發隊列:它也很適合 Dispatch Group 和完成時通知。
Dispatch Group,第二種方式
上面的一切都很好,但在另一個隊列上異步調度然后使用 dispatch_group_wait 來阻塞實在顯得有些笨拙。是的,還有另一種方式……
在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的實現替換它:
下面解釋新的異步方法如何工作:
1. 在新的實現里,因為你沒有阻塞主線程,所以你并不需要將方法包裹在 async 調用中。
2. 同樣的 enter 方法,沒做任何修改。
3. 同樣的 leave 方法,也沒做任何修改。
4. dispatch_group_notify 以異步的方式工作。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那么 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所需要的。
對于這個特定的工作,上面的處理明顯更清晰,而且也不會阻塞任何線程。
太多并發帶來的風險
既然你的工具箱里有了這些新工具,你大概做任何事情都想使用它們,對吧??
看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已經注意到這里的 for 循環,它迭代三次,下載三個不同的圖片。你的任務是嘗試讓 for 循環并發運行,以提高其速度。
dispatch_apply 剛好可用于這個任務。
dispatch_apply 表現得就像一個 for 循環,但它能并發地執行不同的迭代。這個函數是同步的,所以和普通的 for 循環一樣,它只會在所有工作都完成后才會返回。
當在 Block 內計算任何給定數量的工作的最佳迭代數量時,必須要小心,因為過多的迭代和每個迭代只有少量的工作會導致大量開銷以致它能抵消任何因并發帶來的收益。而被稱為跨越式(striding)的技術可以在此幫到你,即通過在每個迭代里多做幾個不同的工作。
| ?譯者注:大概就能減少并發數量吧,作者是提醒大家注意并發的開銷,記在心里! |
那何時才適合用 dispatch_apply 呢?
1. 自定義串行隊列:串行隊列會完全抵消 dispatch_apply 的功能;你還不如直接使用普通的 for 循環。
2. 主隊列(串行):與上面一樣,在串行隊列上不適合使用 dispatch_apply 。還是用普通的 for 循環吧。
3. 并發隊列:對于并發循環來說是很好選擇,特別是當你需要追蹤任務的進度時。
回到 downloadPhotosWithCompletionBlock: 并用下列實現替換它:
你的循環現在是并行運行的了;在上面的代碼中,在調用 dispatch_apply 時,你用第一次參數指明了迭代的次數,用第二個參數指定了任務運行的隊列,而第三個參數是一個 Block。
要知道雖然你有代碼保證添加相片時線程安全,但圖片的順序卻可能不同,這取決于線程完成的順序。
編譯并運行,然后從 “Le Internet” 添加一些照片。注意到區別了嗎?
在真機上運行新代碼會稍微更快的得到結果。但我們所做的這些提速工作真的值得嗎?
實際上,在這個例子里并不值得。下面是原因:
1. 你創建并行運行線程而付出的開銷,很可能比直接使用 for 循環要多。若你要以合適的步長迭代非常大的集合,那才應該考慮使用 dispatch_apply。
2. 你用于創建應用的時間是有限的——除非實在太糟糕否則不要浪費時間去提前優化代碼。如果你要優化什么,那去優化那些明顯值得你付出時間的部分。你可以通過在 Instruments 里分析你的應用,找出最長運行時間的方法??纯?如何在 Xcode 中使用 Instruments?可以學到更多相關知識。
3. 通常情況下,優化代碼會讓你的代碼更加復雜,不利于你自己和其他開發者閱讀。請確保添加的復雜性能換來足夠多的好處。
記住,不要在優化上太瘋狂。你只會讓你自己和后來者更難以讀懂你的代碼。
GCD 的其他趣味
等一下!還有更多!有一些額外的函數在不同的道路上走得更遠。雖然你不會太頻繁地使用這些工具,但在對的情況下,它們可以提供極大的幫助。
阻塞——正確的方式
這可能聽起來像是個瘋狂的想法,但你知道 Xcode 已有了測試功能嗎?:] 我知道,雖然有時候我喜歡假裝它不存在,但在代碼里構建復雜關系時編寫和運行測試非常重要。
Xcode 里的測試在 XCTestCase 的子類上執行,并運行任何方法簽名以 test 開頭的方法。測試在主線程運行,所以你可以假設所有測試都是串行發生的。
當一個給定的測試方法運行完成,XCTest 方法將考慮此測試已結束,并進入下一個測試。這意味著任何來自前一個測試的異步代碼會在下一個測試運行時繼續運行。
網絡代碼通常是異步的,因此你不能在執行網絡獲取時阻塞主線程。也就是說,整個測試會在測試方法完成之后結束,這會讓對網絡代碼的測試變得很困難。也就是,除非你在測試方法內部阻塞主線程直到網絡代碼完成。
| ?注意:有一些人會說,這種類型的測試不屬于集成測試的首選集(Preferred Set)。一些人會贊同,一些人不會。但如果你想做,那就去做。 |
?
導航到 GooglyPuffTests.m 并查看 downloadImageURLWithString:,如下:
這是一種測試異步網絡代碼的幼稚方式。 While 循環在函數的最后一直等待,直到 isFinishedDownloading 布爾值變成 True,它只會在 Completion Block 里發生。讓我們看看這樣做有什么影響。
通過在 Xcode 中點擊 Product / Test 運行你的測試,如果你使用默認的鍵綁定,也可以使用快捷鍵 ?+U 來運行你的測試。
在測試運行時,注意 Xcode debug 導航欄里的 CPU 使用率。這個設計不當的實現就是一個基本的 自旋鎖 。它很不實用,因為你在 While 循環里浪費了珍貴的 CPU 周期;而且它也幾乎沒有擴展性。
| 譯者注:所謂自旋鎖,就是某個線程一直搶占著 CPU 不斷檢查以等到它需要的情況出現。因為現代操作系統都是可以并發運行多個線程的,所以它所等待的那個線程也有機會被調度執行,這樣它所需要的情況早晚會出現。 |
你可能需要使用前面提到的 Network Link Conditioner ,已便清楚地看到這個問題。如果你的網絡太快,那么自旋只會在很短的時間里發生,難以觀察。
| 譯者注:作者反復提到網速太快,而我們還需要對付 GFW,簡直淚流滿面! |
你需要一個更優雅、可擴展的解決方案來阻塞線程直到資源可用。歡迎來到信號量。
信號量
信號量是一種老式的線程概念,由非常謙卑的 Edsger W. Dijkstra 介紹給世界。信號量之所以比較復雜是因為它建立在操作系統的復雜性之上。
如果你想學到更多關于信號量的知識,看看這個鏈接它更細致地討論了信號量理論。如果你是學術型,那可以看一個軟件開發中經典的哲學家進餐問題,它需要使用信號量來解決。
信號量讓你控制多個消費者對有限數量資源的訪問。舉例來說,如果你創建了一個有著兩個資源的信號量,那同時最多只能有兩個線程可以訪問臨界區。其他想使用資源的線程必須在一個…你猜到了嗎?…FIFO隊列里等待。
讓我們來使用信號量吧!
打開 GooglyPuffTests.m 并用下列實現替換 downloadImageURLWithString:
下面來說明你代碼中的信號量是如何工作的:
1. 創建一個信號量。參數指定信號量的起始值。這個數字是你可以訪問的信號量,不需要有人先去增加它的數量。(注意到增加信號量也被叫做發射信號量)。譯者注:這里初始化為0,也就是說,有人想使用信號量必然會被阻塞,直到有人增加信號量。
2. 在 Completion Block 里你告訴信號量你不再需要資源了。這就會增加信號量的計數并告知其他想使用此資源的線程。
3. 這會在超時之前等待信號量。這個調用阻塞了當前線程直到信號量被發射。這個函數的一個非零返回值表示到達超時了。在這個例子里,測試將會失敗因為它以為網絡請求不會超過 10 秒鐘就會返回——一個平衡點!
再次運行測試。只要你有一個正常工作的網絡連接,這個測試就會馬上成功。請特別注意 CPU 的使用率,與之前使用自旋鎖的實現作個對比。
關閉你的網絡鏈接再運行測試;如果你在真機上運行,就打開飛行模式。如果你的在模擬器里運行,你可以直接斷開 Mac 的網絡鏈接。測試會在 10 秒后失敗。這很棒,它真的能按照預想的那樣工作!
還有一些瑣碎的測試,但如果你與一個服務器組協同工作,那么這些基本的測試能夠防止其他人就最新的網絡問題對你說三道四。
使用 Dispatch Source
GCD 的一個特別有趣的特性是 Dispatch Source,它基本上就是一個低級函數的 grab-bag ,能幫助你去響應或監測 Unix 信號、文件描述符、Mach 端口、VFS 節點,以及其它晦澀的東西。所有這些都超出了本教程討論的范圍,但你可以通過實現一個 Dispatch Source 對象并以一個相當奇特的方式來使用它來品嘗那些晦澀的東西。
第一次使用 Dispatch Source 可能會迷失在如何使用一個源,所以你需要知曉的第一件事是 dispatch_source_create 如何工作。下面是創建一個源的函數原型:
第一個參數是 dispatch_source_type_t 。這是最重要的參數,因為它決定了 handle 和 mask 參數將會是什么。你可以查看?Xcode 文檔?得到哪些選項可用于每個 dispatch_source_type_t 參數。
下面你將監控 DISPATCH_SOURCE_TYPE_SIGNAL 。如文檔所顯示的:
一個監控當前進程信號的 Dispatch Source。 handle 是信號編號,mask 未使用(傳 0 即可)。
這些 Unix 信號組成的列表可在頭文件?signal.h?中找到。在其頂部有一堆 #define 語句。你將監控此信號列表中的 SIGSTOP 信號。這個信號將會在進程接收到一個無法回避的暫停指令時被發出。在你用 LLDB 調試器調試應用時你使用的也是這個信號。
去往 PhotoCollectionViewController.m 并添加如下代碼到 viewDidLoad 的頂部,就在 [super viewDidLoad] 下面:
這些代碼有點兒復雜,所以跟著注釋一步步走,看看到底發生了什么:
1. 最好是在 DEBUG 模式下編譯這些代碼,因為這會給“有關方面(Interested Parties)”很多關于你應用的洞察。
2. Just to mix things up,你創建了一個 dispatch_queue_t 實例變量而不是在參數上直接使用函數。當代碼變長,分拆有助于可讀性。
3. 你需要 source 在方法范圍之外也可被訪問,所以你使用了一個 static 變量。
4. 使用 weakSelf 以確保不會出現保留環(Retain Cycle)。這對 PhotoCollectionViewController 來說不是完全必要的,因為它會在應用的整個生命期里保持活躍。然而,如果你有任何其它會消失的類,這就能確保不會出現保留環而造成內存泄漏。
5. 使用 dispatch_once 確保只會執行一次 Dispatch Source 的設置。
6. 初始化 source 變量。你指明了你對信號監控感興趣并提供了 SIGSTOP 信號作為第二個參數。進一步,你使用主隊列處理接收到的事件——很快你就好發現為何要這樣做。
7. 如果你提供的參數不合格,那么 Dispatch Source 對象不會被創建。也就是說,在你開始在其上工作之前,你需要確保已有了一個有效的 Dispatch Source 。
8. 當你收到你所監控的信號時,dispatch_source_set_event_handler 就會執行。之后你可以在其 Block 里設置合適的邏輯處理器(Logic Handler)。
9. 一個基本的 NSLog 語句,它將對象打印到控制臺。
10.默認的,所有源都初始為暫停狀態。如果你要開始監控事件,你必須告訴源對象恢復活躍狀態。
編譯并運行應用;在調試器里暫停并立即恢復應用,查看控制臺,你會看到這個來自黑暗藝術的函數確實可以工作。你看到的大概如下:
你的應用現在具有調試感知了!這真是超級棒,但在真實世界里該如何使用它呢?
你可以用它去調試一個對象并在任何你想恢復應用的時候顯示數據;你同樣能給你的應用加上自定義的安全邏輯以便在惡意攻擊者將一個調試器連接到你的應用上時保護它自己(或用戶的數據)。
| ?譯者注:好像挺有用! |
一個有趣的主意是,使用此方式的作為一個堆棧追蹤工具去找到你想在調試器里操縱的對象。?
稍微想想這個情況。當你意外地停止調試器,你幾乎從來都不會在所需的棧幀上。現在你可以在任何時候停止調試器并在你所需的地方執行代碼。如果你想在你的應用的某一點執行的代碼非常難以從調試器訪問的話,這會非常有用。有機會試試吧!?
將一個斷點放在你剛添加在 viewDidLoad 里的事件處理器的 NSLog 語句上。在調試器里暫停,然后再次開始;應用會到達你添加的斷點?,F在你深入到你的 PhotoCollectionViewController 方法深處。你可以訪問 PhotoCollectionViewController 的實例得到你關心的內容。非常方便!
| 注意:如果你還沒有注意到在調試器里的是哪個線程,那現在就看看它們。主線程總是第一個被 libdispatch 跟隨,它是 GCD 的坐標,作為第二個線程。之后,線程計數和剩余線程取決于硬件在應用到達斷點時正在做的事情。 |
在調試器里,鍵入命令:
然后恢復應用的執行。你會看到如下內容:
??
使用這個方法,你可以更新 UI、查詢類的屬性,甚至是執行方法——所有這一切都不需要重啟應用并到達某個特定的工作狀態。相當優美吧!
| ?譯者注:發揮這一點,是可以做出一些調試庫的吧? |
之后又該往何處去?
你可以在此下載最終的項目。
我討厭再次提及此主題,但你真的要看看?如何使用 Instruments?教程。如果你計劃優化你的應用,那你一定要學會使用它。請注意 Instruments 擅長于分析相對執行:比較哪些區域的代碼相對于其它區域的代碼花費了更長的時間。如果你嘗試計算出某個方法實際的執行時間,那你可能需要拿出更多的自釀的解決方案(Home-brewed Solution)。
同樣請看看?如何使用 NSOperations 和 NSOperationQueues?吧,它們是建立在 GCD 之上的并發技術。大體來說,如果你在寫簡單的用過就忘的任務,那它們就是使用 GCD 的最佳實踐,。NSOperations 提供更好的控制、處理大量并發操作的實現,以及一個以速度為代價的更加面向對象的范例。
記住,除非你有特別的原因要往下流走(譯者的玩笑:即使用低級別 API),否則永遠應嘗試并堅持使用高級的 API。如果你想學到更多或想做某些非常非常“有趣”的事情,那你就應該冒險進入 Apple 的黑暗藝術。
推薦閱讀:GCD 深入理解(一)
CocoaChina是全球最大的蘋果開發中文社區,官方微信每日定時推送各種精彩的研發教程資源和工具,介紹app推廣營銷經驗,最新企業招聘和外包信息,以及Cocos2d引擎、Cocos Studio開發工具包的最新動態及培訓信息。關注微信可以第一時間了解最新產品和服務動態,微信在手,天下我有!
?
http://www.cocoachina.com/industry/20140428/8248.html轉載于:https://www.cnblogs.com/ldnh/p/5276295.html
總結
- 上一篇: 用官方2012版本131兆,一共有四个自
- 下一篇: 计划将项目中使用entity frame