- 原文連結 : The GCD Handbook
- 原文作者 : Soroush
- 譯文出自 : 掘金翻譯計劃
- 譯者 : LoneyIsError
- 校對者:woopqww111hsinshufan
Grand Central Dispatch,或者GCD,是一個極其強大的工具。它給你一些底層的元件,像佇列和訊號量,讓你可以通過一些有趣的方式來獲得有用的多執行緒效果。可惜的是,這個基於C的API是一個有點神祕,它不會明顯的告訴你如何使用這個底層元件來實現更高層次的方法。在這篇文章中,我希望描述那些你可以通過GCD提供給你的底層元件來實現的一些用法。
後臺工作
也許最簡單的用法,GCD讓你在後臺執行緒上做一些工作,然後回到主執行緒繼續處理,因為像那些屬於 UIKit
的元件只能(主要)在主執行緒中使用。
在本指南中,我將使用 doSomeExpensiveWork()
方法來表示一些長時間執行的有返回值的任務。
這種模式可以像這樣建立起來:
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;
}
複製程式碼
扁平化一個完整的block
現在 GCD 開始變得有趣了。使用一個訊號量,我們可以讓一個執行緒暫停任意時間,直到另一個執行緒向它傳送一個訊號。這個訊號量,就像 GCD 其餘部分一樣,是執行緒安全的,並且他們可以從任何地方被觸發。
當你需要去同步一個你不能修改的非同步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
,開啟訊號量,並允許序列佇列繼續。
限制併發 block 的數量。
在前面的例子中,訊號量作為一個簡單的標誌,但它也可以被用來作為一種有限的資源計數器。如果你想在一個特定資源上開啟特定數量的連線,你可以使用下面的程式碼:
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的高階用法而我沒有羅列在這裡,我喜歡聽到它們並將它們新增到列表中。