GCD和Operation/OperationQueue 看這一篇文章就夠了

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
15235190270597.jpg

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的可實例化子類BlockOperationInvocationOperation, 這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可以查看處理完成的圖片:

15235348249665.jpg

從日志輸出可以看出, 總的圖片處理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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容