(翻譯)給iOS開發者的GCD使用手冊

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_syncdispatch_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 其他部分同樣,是線程安全的,而且他們能夠從任何地方被觸發。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 ,開啓信號量,並容許串行隊列繼續。

限制併發 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表示資源已被釋放。若是有任務阻塞和等待資源,其中一個隨即被放行並進行它的工做。

其效果相似於 maxConcurrentOperationCountNSOperationQueue 。若是你使用原 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_asyncbarrier 部分,意味着它將等待直到當前運行隊列中的每一個 block 執行完畢後才執行。其餘 block 將在它後面排隊,當barrier調度完成時執行。

總結

Grand Central Dispatch 是一個有不少底層語言的框架。使用它們,這個是我能創建的比較高級的技術。若是有其餘一些你使用的GCD的高級用法而我沒有羅列在這裏,我喜歡聽到它們並將它們添加到列表中。

相關文章
相關標籤/搜索