Grand Central Dispatch簡稱GCD,是蘋果公司為多核的并行運算提出的解決方案, 允許程序?qū)⑷蝿?wù)切分為多個單一任務(wù)然后提交至工作隊列來并發(fā)地或者串行地執(zhí)行。GCD會自動利用更多的CPU內(nèi)核(比如雙核、四核), 自動管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程).
下面逐一介紹DispatchQueue, Operation和OperationQueue.
文中的示例代碼均可參見我的GitHub: https://github.com/zhihuitang/GCDExample
1. DispatchQueue
GCD的基本概念就是DispatchQueue。DispatchQueue是一個對象,它可以接受任務(wù),并將任務(wù)以FIFO(先到先執(zhí)行)的順序來執(zhí)行。DispatchQueue可以是并發(fā)的或串行的, 它有3種隊列類型:
- Main queue
- Global queue
- Custom queue

1.1 Main queue(串行Serial)
Main queue運行在系統(tǒng)主線程.它是一個串行隊列,我們所有的UI刷新都發(fā)生在Main queue. 獲取Main queue的方法很簡單:
// Get the main queue
let mainQueue = DispatchQueue.main
如果要在非Main queue線程中直接刷新UI, 運行時會出exception. 一般的做法是將代碼放在Main queue中異步執(zhí)行:
let mainQueue = DispatchQueue.main
mainQueue.async {
// UI刷新代碼在這里
}
1.2 Global queues(并行Concurrent)
如果要在后臺執(zhí)行非UI相關(guān)的工作, 一般把這部分工作放在Global queue. Global queue是一種系統(tǒng)內(nèi)共享的并行的隊列. 申請Global queue的方法很簡單:
// Get the .userInitiated global dispatch queue
let userQueue = DispatchQueue.global(qos: .userInitiated)
// Get the .default global dispatch queue
let defaultQueue = DispatchQueue.global()
Global queue隊列有四種優(yōu)先級: 高, 缺省, 低, 后臺. 在實際申請Global queue時,我們不需直接指定優(yōu)先級, 只需申明所需的(QoS)類型, Qos間接的決定了這些queue的優(yōu)先級
例如:
DispatchQueue.global(qos: .userInteractive)
DispatchQueue.global(qos: .userInitiated)
DispatchQueue.global() // .default qos
DispatchQueue.global(qos: .utility)
DispatchQueue.global(qos: .background)
DispatchQueue.global(qos: .unspecified)
The QoS classes are:
User-interactive
表示為了給用戶提供一個比較好的體驗, 任務(wù)必須立即完成. 主要用于UI刷新, 低延遲的事件處理等. 在整個App內(nèi), 這種類型的任務(wù)不宜太多. 它是最高優(yōu)先級的.User-initiated
用于UI發(fā)起異步任務(wù),用戶在等待執(zhí)行的結(jié)果, 這種queue是高優(yōu)先級的.
DispatchQueue.global(qos: .userInitiated).async {
let overlayImage = self.faceOverlayImageFromImage(self.image)
DispatchQueue.main.async {
self.fadeInNewImage(overlayImage)
}
}
Utility
長時間運行的任務(wù), 典型情況是App中會有一個進度條表示任務(wù)的進度. 主要用于 計算, I/O, 網(wǎng)絡(luò)交互等, 主要為節(jié)能考慮. 這種queue是低優(yōu)先級的.Background
任務(wù)在運行, 但用戶感覺不到它在運行的場景. 主要用于不需要用戶干涉,對時間不敏感的獲取數(shù)據(jù)等任務(wù), 這種queue是后臺優(yōu)先級,屬于最低優(yōu)先級的那一種.
1.3 Custom queues
用戶創(chuàng)建的Custom queues默認的是串行的, 如果指定了attributes為concurrent則為并行的. 下面我們用代碼演示串行隊列/并行隊列的區(qū)別.
-
Serial Queue
除了DispatchQueue.main是并行的queue外, 你也可以創(chuàng)建自己的并行queue(缺省為串行)
func task1() {
print("Task 1 started")
// make task1 take longer than task2
sleep(1)
print("Task 1 finished")
}
func task2() {
print("Task 2 started")
print("Task 2 finished")
}
example(of: "Serial Queue") {
let mySerialQueue = DispatchQueue(label: "com.crafttang.serialqueue")
mySerialQueue.async {
task1()
}
mySerialQueue.async {
task2()
}
}
sleep(2)
上面代碼的輸出為:
--- Example of: Serial Queue ---
Task 1 started
[ spent: 0.00026 ] seconds
Task 1 finished
Task 2 started
Task 2 finished
從上可以看出, task1和task2是順序運行的, 只有task1執(zhí)行完了后task2才可以執(zhí)行.由于task1和task2都是異步(asyc)執(zhí)行的, 所以不會阻塞當前線程, 2個任務(wù)執(zhí)行的時間只有0.00026秒.
-
Concurrent Queue
創(chuàng)建用戶自己的并行queue, 聲明.concurrent屬性即可:
example(of: "Concurrent Queue") {
let concurrentQueue = DispatchQueue(label: "com.crafttang.currentqueue", attributes: .concurrent)
concurrentQueue.async {
task1()
}
concurrentQueue.async {
task2()
}
}
sleep(2)
上面的輸出為:
--- Example of: Concurrent Queue ---
Task 1 started
Task 2 started
Task 2 finished
[ spent: 0.00034 ] seconds
Task 1 finished
從上面可以看出, task1和task2是并發(fā)執(zhí)行的, task1啟動后, 由于執(zhí)行時間需要1s, 這個時候task2也可以同步運行, 所以我們可以看到task1啟動后, 立即啟動task2, 然后task2完成, task1完成.
task1和task2都是異步(async)運行的,所以它們花費的時間仍然很短, 只有0.00034秒.
同步(Synchronous) vs. 異步(Asynchronous)
對于一個任務(wù)(function), 可以在GCD隊列里同步運行, 也可以異步運行.
同步運行的任務(wù), 不開啟新的線程, 會阻塞當前線程, 等任務(wù)完成才返回.
異步運行的任務(wù), 會開啟新的線程, 不會阻塞當前線程, 分發(fā)任務(wù)后立即返回,不用等任務(wù)完成.
2. DispatchGroup
DispatchGroup實例用來追蹤不同隊列中的不同任務(wù)。當group里所有事件都完成GCD API有兩種方式發(fā)送通知,第一種是DispatchGroup.wait,會阻塞當前進程,等所有任務(wù)都完成或等待超時。第二種方法是使用DispatchGroup.notify,異步執(zhí)行閉包,不會阻塞當前線程。
example(of: "DispatchGroup") {
let workerQueue = DispatchQueue(label: "com.crafttang.dispatchgroup", attributes: .concurrent)
let dispatchGroup = DispatchGroup()
let numberArray: [(Any,Any)] = [("A", "B"), (2,3), ("C", "D"), (6,7), (8,9)]
for inValue in numberArray {
workerQueue.async(group: dispatchGroup) {
let result = slowJoint(inValue)
print("Result = \(result)")
}
}
//dispatchGroup.wait(timeout: .now() + 1)
let notifyQueue = DispatchQueue.global()
dispatchGroup.notify(queue: notifyQueue) {
print(" ?? joint tasks finished")
}
}
以上程序的輸出如下:
--- Example of: DispatchGroup ---
[ spent: 0.00406 ] seconds
Result = CD
Result = AB
Result = 23
Result = 67
Result = 89
?? joint tasks finished
3. Operation/OperationQueue
GCD是一個底層的C API, OperationQueue是基于GCD和隊列模型的一個抽象,負責Operation的調(diào)度.這意味著我們可以像GCD那樣并行的執(zhí)行任務(wù), 但是以面向?qū)ο蟮姆绞?
GCD和OperationQueue主要差別如下:
- OperationQueue不遵循FIFO: 相比于GCD, OperationQueue使用起來更加靈活. 在OperationQueue中,我們可以這是Operation之間的依賴, 例如Operation-B任務(wù)只有在Operation-A執(zhí)行完成之后才能開始執(zhí)行. 這也是OperationQueue不遵循FIFO的原因.
- OperationQueue并行運行: OperationQueue中的任務(wù)(Operation)并行執(zhí)行, 你不能設(shè)置成串行Serial. 但是可以有個變通方法達到串行運行的效果,就是設(shè)置依賴, C依賴B, B依賴A; 所以他們運行的順序就是
A -> B -> C - 一個獨立的Operation任務(wù)是同步運行的, 如果想讓Operation異步運行, 你必須將Operation加入到一個OperationQueue.
- Operation queues 是OperationQueue 的一個實例, 實際上Operation queue是被封裝在一個Operation中運行的.
3.1 Operation
提交到OperationQueue中的任務(wù)必須是一個Operation實例.你可以簡單的認為Operaion就是一項工作/任務(wù). Operation是一個不能直接使用的抽象類,在實際使用中你必須用Operation的子類.
open class Operation : NSObject {
open func start()
open func main()
open var isCancelled: Bool { get }
open func cancel()
open var isExecuting: Bool { get }
open var isFinished: Bool { get }
open var isConcurrent: Bool { get }
@available(iOS 7.0, *)
open var isAsynchronous: Bool { get }
open var isReady: Bool { get }
open func addDependency(_ op: Operation)
open func removeDependency(_ op: Operation)
open var dependencies: [Operation] { get }
open var queuePriority: Operation.QueuePriority
@available(iOS 4.0, *)
open var completionBlock: (() -> Swift.Void)?
@available(iOS 4.0, *)
open func waitUntilFinished()
@available(iOS, introduced: 4.0, deprecated: 8.0, message: "Not supported")
open var threadPriority: Double
@available(iOS 8.0, *)
open var qualityOfService: QualityOfService
@available(iOS 8.0, *)
open var name: String?
}
在iOS SDK中, 蘋果提供了2種Operation的可實例化子類BlockOperation 和 InvocationOperation, 這2種類我們可以直接使用.
-
BlockOperation. 對
DispatchQueue.global()的一個封裝, 它可以管理一個或多個blocks, bocks異步,并行的執(zhí)行, 如果你想串行的執(zhí)行任務(wù), 你可以設(shè)置依賴或選擇Custom queues(參見DispatchQueue).BlockOperation為一些已經(jīng)使用了OperationQueue但不想使用DispatchQueue的App, 提供一種面向?qū)ο蟮姆庋b. 作為一種Operation, 相比DisptachQueue, 它提供了更多的特性例如添加依賴, KVO通知, 取消任務(wù)等. 某種程度上BlockOperation也像一個一個DispatchGroup: 所有的blocks完成后它會收到通知. - 如果BlockOperation中只有一個任務(wù), 那么這個任務(wù)會在當前線程中. 如果有多個任務(wù), 那么系統(tǒng)可能會開啟多個線程來執(zhí)行這些任務(wù)
example(of: "BlockOperation") {
let blockOperation = BlockOperation()
for i in 1...10 {
blockOperation.addExecutionBlock {
sleep(2)
print("\(i) in blockOperation: \(Thread.current)")
}
}
blockOperation.completionBlock = {
print("All block operation task finished: \(Thread.current)")
}
blockOperation.start()
}
The output is as fellows:
--- Example of: BlockOperation ---
8 in blockOperation: <NSThread: 0x604000074180>{number = 11, name = (null)}
6 in blockOperation: <NSThread: 0x60c000072d80>{number = 1, name = main}
4 in blockOperation: <NSThread: 0x608000079e40>{number = 7, name = (null)}
3 in blockOperation: <NSThread: 0x604000073e00>{number = 8, name = (null)}
2 in blockOperation: <NSThread: 0x60c0000774c0>{number = 9, name = (null)}
5 in blockOperation: <NSThread: 0x600000078ec0>{number = 6, name = (null)}
1 in blockOperation: <NSThread: 0x60800007af40>{number = 10, name = (null)}
7 in blockOperation: <NSThread: 0x604000072b80>{number = 5, name = (null)}
10 in blockOperation: <NSThread: 0x60c000072d80>{number = 1, name = main}
9 in blockOperation: <NSThread: 0x604000074180>{number = 11, name = (null)}
All block operation task finished: <NSThread: 0x604000072b80>{number = 5, name = (null)}
[ spent: 4.00875 ] seconds
加入到blockOperation的每個block需要花費2s, 從輸出日志可以看出, 10個blocks運行完花費了約4s. 說明這些block是并行運行的,至少有5個線程同時運行.
- InvocationOperation – 已經(jīng)從Swift中移除了,無需關(guān)注
關(guān)于自定義的Operation, 下面以一個例子介紹該如何使用. 這個例子是一個Operation, 將我女朋友的照片模糊化后輸出.
首先定義一個類,繼承與Operation:
class BlurImageOperation: Operation {
var inputImage: UIImage?
var outputImage: UIImage?
override func main() {
outputImage = blurImage(image: inputImage)
}
}
其中blurImage(image: inputImage)是將輸入的圖片模糊化, 它是一個耗時任務(wù).
然后使用這個BlurImageOperation:
let operation = BlurImageOperation()
operation.inputImage = inputImage
operation.start()
operation.outputImage
注意Operation啟動的方式有2中, 要么手動.start()啟動,要么將Operation加入到OperationQueue中自動啟動.這里我們沒用OperationQueue,所以要手動啟動.
Operation本身是同步執(zhí)行的, 所以operation.start()會阻塞在這里, 可能會花較長時間. operation執(zhí)行完成后, 在Playground中點擊operation.outputImage對應(yīng)的側(cè)邊欄上的小眼睛Quick Look可以查看處理完成的圖片:

從日志輸出可以看出, 總的圖片處理BlurImageOperation花費了4.9s
--- Example of: Operation ---
[ spent: 4.96002 ] seconds
3.2 OperationQueue
OperationQueue負責Operation任務(wù)集的調(diào)度, Operation加入OperationQueue后, 立即啟動運行, 無需手工啟動. 通過maxConcurrentOperationCount設(shè)置并發(fā)任務(wù)的數(shù)量:
// DONE: Create printerQueue
let operationQueue = OperationQueue()
// DONE later: Set maximum to 2
operationQueue.maxConcurrentOperationCount = 2
下面一個具體的例子來說明OperationQueue的用法.
example(of: "OperationQueue") {
let printerQueue = OperationQueue()
//printerQueue.maxConcurrentOperationCount = 2
printerQueue.addOperation { print("厲"); sleep(3) }
printerQueue.addOperation { print("害"); sleep(3) }
printerQueue.addOperation { print("了"); sleep(3) }
printerQueue.addOperation { print("我"); sleep(3) }
printerQueue.addOperation { print("的"); sleep(3) }
printerQueue.addOperation { print("哥"); sleep(3) }
//阻塞在這里
printerQueue.waitUntilAllOperationsAreFinished()
}
這個例子中, 申明了一個 printerQueue, 然后往里添加了6個任務(wù)Operation, 每個Operation輸出一個字后sleep 3秒. 執(zhí)行結(jié)果如下:
--- Example of: OperationQueue ---
厲
害
了
我
的
哥
[ spent: 3.00513 ] seconds
可以看出, 每個Operation都要花3秒, 而printerQueue實際也只花費了3秒, 說明這些Operation都是并行執(zhí)行的.
輸出的順序雖然和Operation加入的順序是一樣的,這其實是一種巧合, 實際情況不一定是這樣的. 如果你將maxConcurrentOperationCount 設(shè)置為2(取消上面的注釋行),
printerQueue.maxConcurrentOperationCount = 2
你會發(fā)現(xiàn)輸出是這樣的:
--- Example of: OperationQueue ---
厲
害
我
了
哥
的
[ spent: 9.01079 ] seconds
每個Operation花費3秒, 最大并發(fā)數(shù)為2, 兩兩一組可分為3組, 6個Operation總共花費了3*3 = 9秒, 輸出順序和加入Operation的順序不完全一樣,進一步說明加入OperationQueue的Operation是concurrent執(zhí)行的.
文中的示例代碼均可參見我的GitHub: https://github.com/zhihuitang/GCDExample