- 原文連接 : The GCD Handbook
Grand Central Dispatch,或者GCD,是一個極其強大的工具。它給你一些底層的組件,像隊列和信號量,讓你能夠經過一些有趣的方式來得到有用的多線程效果。惋惜的是,這個基於C的API是一個有點神祕,它不會明顯的告訴你如何使用這個底層組件來實現更高層次的方法。在這篇文章中,我但願描述那些你能夠經過GCD提供給你的底層組件來實現的一些用法。html
也許最簡單的用法,GCD讓你在後臺線程上作一些工做,而後回到主線程繼續處理,由於像那些屬於 UIKit
的組件只能(主要)在主線程中使用。ios
在本指南中,我將使用 doSomeExpensiveWork()
方法來表示一些長時間運行的有返回值的任務。git
這種模式能夠像這樣創建起來:github
let defaultPriority = DISPATCH_QUEUE_PRIORITY_DEFAULT
let backgroundQueue = dispatch_get_global_queue(defaultPriority, 0)
dispatch_async(backgroundQueue, {
let result = doSomeExpensiveWork()
dispatch_async(dispatch_get_main_queue(), {
//use `result` somehow
})
})
複製代碼
在實踐中,我從不使用任何隊列優先級除了 DISPATCH_QUEUE_PRIORITY_DEFAULT
。這返回一個隊列,它能夠支持數百個線程的執行。若是你的耗性能的工做老是在一個特定的後臺隊列中發生,你也可用經過 dispatch_queue_create
方法來建立本身的隊列。 dispatch_queue_create
能夠建立一個任意名稱的隊列,不管它是串行的仍是並行的。安全
注意每個調用使用 dispatch_async
,不使用 dispatch_sync
。dispatch_async
在 block 執行前返回,而 dispatch_sync
會等到 block 執行完畢才返回。內部的調用可使用 dispatch_sync
(由於無論它何時返回),但外部必須調用 dispatch_async
(不然,主線程會被阻塞)。多線程
dispatch_once
是一個能夠被用來建立單例的API。在 Swift 中它再也不是必要的,由於 Swift 中有一個更簡單的方法來建立單例。爲了之後,固然,我把它寫在這裏(用 Objective-C )。併發
+ (instancetype) sharedInstance {
static dispatch_once_t onceToken;
static id sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
複製代碼
如今 GCD 開始變得有趣了。使用一個信號量,咱們可讓一個線程暫停任意時間,直到另外一個線程向它發送一個信號。這個信號量,就像 GCD 其他部分同樣,是線程安全的,而且他們能夠從任何地方被觸發。app
當你須要去同步一個你不能修改的異步API時,你可使用信號量解決問題。框架
// on a background queue
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
//the expensive asynchronous work is now done
複製代碼
dispatch_semaphore_wait
會阻塞線程直到 dispatch_semaphore_signal
被調用。這就意味着 signal
必定要在另一個線程中被調用,由於當前線程被徹底阻塞。此外,你不該該在在主線程中調用 wait
,只能在後臺線程。異步
在調用 dispatch_semaphore_wait
時你能夠選擇任意的超時時間,可是我傾向於一直使用 DISPATCH_TIME_FOREVER
。
這可能不是徹底顯而易見的,爲何你要把已有的一個完整的 block 代碼變爲扁平化,但它確實很方便。我最近使用的一種狀況是,執行一系列必須連續發生的異步任務。這個使用這種方式的簡單抽象被稱做 AsyncSerialWorker
:
typealias DoneBlock = () -> ()
typealias WorkBlock = (DoneBlock) -> ()
class AsyncSerialWorker {
private let serialQueue = dispatch_queue_create("com.khanlou.serial.queue", DISPATCH_QUEUE_SERIAL)
func enqueueWork(work: WorkBlock) {
dispatch_async(serialQueue) {
let semaphore = dispatch_semaphore_create(0)
work({
dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
}
複製代碼
這一小類能夠建立一個串行隊列,並容許你將工做添加到 block 中。當你的工做完成後, WorkBlock
會調用 DoneBlock
,開啓信號量,並容許串行隊列繼續。
在前面的例子中,信號量做爲一個簡單的標誌,但它也能夠被用來做爲一種有限的資源計數器。若是你想在一個特定資源上打開特定數量的鏈接,你可使用下面的代碼:
class LimitedWorker {
private let concurrentQueue = dispatch_queue_create("com.khanlou.concurrent.queue", DISPATCH_QUEUE_CONCURRENT)
private let semaphore: dispatch_semaphore_t
init(limit: Int) {
semaphore = dispatch_semaphore_create(limit)
}
func enqueueWork(work: () -> ()) {
dispatch_async(concurrentQueue) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
work()
dispatch_semaphore_signal(semaphore)
}
}
}
複製代碼
這個例子從蘋果的Concurrency Programming Guide拿來的。他們能夠更好的解釋在這裏發生了什麼:
當你建立一個信號量時,你能夠指定你的可用資源的數量。這個值是信號量的初始計數變量。你每一次等待信號量發送信號時,這個
dispatch_semaphore_wait
方法使計數變量遞減1。若是產生的值是負的,則函數告訴內核來阻止你的線程。在另外一端,這個dispatch_semaphore_signal
函數遞增count變量用1表示資源已被釋放。若是有任務阻塞和等待資源,其中一個隨即被放行並進行它的工做。
其效果相似於 maxConcurrentOperationCount
在 NSOperationQueue
。若是你使用原 GCD隊 列而不是 NSOperationQueue
,你可使用信號莊主來限制同時執行的 block 數量。
一個值得注意的就是,每次你調用 enqueueWork
,若是你打開信號量的限制,就會啓動一個新線程。若是你有一個低限而且大量工做的隊列,您能夠建立數百個線程。一如既往,先配置文件,而後更改代碼。
若是你有多 block 工做來執行,而且在他們集體完成時你須要發一個通知,你可使用 group 。dispatch_group_async
容許你在隊列中添加工做(在 block 裏面的工做應該是同步的),而且記錄添加了多少了項目。注意,在同一個 dispatch group 中能夠將工做添加到不一樣的隊列中,而且能夠跟蹤它們。當全部跟蹤的工做完成,這個 block 開始運行 dispatch_group_notify
,就像是一個完整的 block 。
dispatch_group_t group = dispatch_group_create()
for item in someArray {
dispatch_group_async(group, backgroundQueue, {
performExpensiveWork(item: item)
})
}
dispatch_group_notify(group, dispatch_get_main_queue(), {
// all the work is complete
}
複製代碼
擁有一個完整的block,對於扁平化一個功能來講是一個很好的案例。 dispatch group 認爲,當它返回時,這個 block 應該完成了,因此你須要這個 block 等待直到其餘工做已經完成。
有更多的手動方式來使用 dispatch groups ,特別是若是你耗性能的工做已是異步的:
// must be on a background thread
dispatch_group_t group = dispatch_group_create()
for item in someArray {
dispatch_group_enter(group)
performExpensiveAsyncWork(item: item, completionBlock: {
dispatch_group_leave(group)
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
// all the work is complete
複製代碼
這段代碼是比較複雜的,但經過一行一行的閱讀能夠幫助理解它。就像信號量,groups 也還保持線程安全,是一個你能夠操做的內部計數器。您可使用此計數器來確保在執行完成 block 以前,多個長的運行任務都已完成。使用 「enter」 遞增計數器,並用 「leave」 遞減計數器。 dispatch_group_async
爲你處理全部的這些細節,因此我願意儘量的使用它。
在這段代碼的最後一點是 wait
方法:它會阻塞線程,並等待計數器爲0後,繼續執行。注意,即便你使用了enter
/leave
API,你也能夠在在隊列中添加一個 dispatch_group_notify
block.反過來也是對的:當你使用 dispatch_group_async
API時你也可使用 dispatch_group_wait
。
dispatch_group_wait
,就像dispatch_semaphore_wait
同樣,能夠設置超時。再一次聲明,DISPATCH_TIME_FOREVER
已很是足夠使用, 我從未以爲須要使用其餘的來設置超時。固然就像 dispatch_semaphore_wait
同樣,永遠不要在主線程使用 dispatch_group_wait
。
二者之間最大的區別是,使用 notify
能夠徹底從主線程調用,而使用 wait
,必須發生在後臺隊列(至少 wait
的部分,由於它會徹底阻塞當前隊列)。
Swift 語言的 Dictionary
(和 Array
)類型都是值類型。 當他們被改變時, 他們的引用會徹底被新的結構給替代。固然,由於更新實例變量的 Swift 對象不是原子性的,它們不是線程安全的。雙線程能夠在同一時間更新一個字典(例如,增長一個值),而且兩個嘗試寫在同一塊內存,這可能致使內存損壞。咱們可使用隔離隊列來實現線程安全。 讓咱們建立一個identity map。 identity map 是一個字典,將項目從其ID
屬性映射到模型對象。
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
func object(forID ID: String) -> T? {
return dictionary[ID] as T?
}
func addObject(object: T) {
dictionary[object.ID] = object
}
}
複製代碼
這個對象基本上是一個字典的包裝器。若是咱們的方法 addObject
同一時間被多個線程所調用,它可能會損害內存,由於這些線程對對同一個引用進行處理。這被稱之爲 readers-writers problem。總之,咱們能夠同時有多個讀者閱讀,可是隻有一個線程能夠在任何給定的時間寫。 幸運的是,GCD 給了咱們很好的工具去處理這樣的狀況。咱們可使用如下四種 API :
dispatch_sync
dispatch_async
dispatch_barrier_sync
dispatch_barrier_async
咱們理想的狀況是,讀同步,同時,而寫能夠異步,當引用該對象時必須是惟一的。 GCD 的 barrier
API集能夠作一些特別的事情:他們執行 block 以前必須等到隊列徹底空了。使用 barrier
API去進行字典寫入的操做將會被限制,這樣確保咱們永遠不會有任何寫入發生在同一時間,不管是讀取或是寫入。
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
let accessQueue = dispatch_queue_create("com.khanlou.isolation.queue", DISPATCH_QUEUE_CONCURRENT)
func object(withID ID: String) -> T? {
var result: T? = nil
dispatch_sync(accessQueue, {
result = dictionary[ID] as T?
})
return result
}
func addObject(object: T) {
dispatch_barrier_async(accessQueue, {
dictionary[object.ID] = object
})
}
}
複製代碼
dispatch_sync
將 block 添加到咱們的隔離隊列,而後等待它在返回以前執行。這樣,咱們就會有咱們的同步閱讀的結果。(若是咱們沒有作到同步,咱們的 getter 方法可能須要一個完成的 block 。)由於 accessQueue
是併發的,這些同步讀取就能同時發生。 dispatch_barrier_async
將 block 添加到隔離隊列。這個 async
部分意味着它將實際執行的 block 以前返回(執行寫入操做)。這對咱們的表現有好處,但也有一個缺點是,在 「write」 操做後當即執行 「read」 操做可能會致使獲取改變以前的舊數據。 這個 dispatch_barrier_async
的 barrier
部分,意味着它將等待直到當前運行隊列中的每一個 block 執行完畢後才執行。其餘 block 將在它後面排隊,當barrier調度完成時執行。
Grand Central Dispatch 是一個有不少底層語言的框架。使用它們,這個是我能創建的比較高級的技術。若是有其餘一些你使用的GCD的高級用法而我沒有羅列在這裏,我喜歡聽到它們並將它們添加到列表中。