Dispatch Queue 教學

Photo by Alex Alvarez on Unsplash
Photo by Alex Alvarez on Unsplash
GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。

Grand Central Dispatch (GCD) 提供有效率的並行(concurrent)處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。我們不再需要擔心是否建立過多的執行緒了!

什麼是 Dispatch Queues?

Dispatch queues 是 Grand Central Dispatch(GCD)的其中一個工具。它讓你可以以非同步(asynchronously)或同步(synchronously)的方式執行一段程式碼。這也就是所謂的並行(concurrency)處理。

一般來說,我們在撰寫並行程式時,不外乎是建立一個新的執行緒來處理並行的程式碼。這就是所謂的多執行緒(multi-threading)。然而,並不是建立越多的執行緒來並行處理很多任務就可以加快應用程式的效能。當執行緒過多時,CPU 會花過多的時間忙於執行緒間的切換處理。而過少的執行緒又無法充分地運用 CPU。那問題是多少的執行緒才是適當的呢?在 Concurrency and Application Design 中提到,適當的執行緒數量必須要根據裝置上 CPU 核心的數量而定。對於應用程式開發者而言,在程式執行期間(runtime),要根據 CPU 和作業系統來算出最佳的執行緒數量,並且即時調整執行緒的數量,是非常困難的事情。此外,管理執行緒的程式碼大同小異,而且你自己的寫的程式碼可能沒有做過最佳化處理。GCD 的程式碼是有最佳化處理過的,可以最有效率地管理執行緒。

GCD 就是用來幫助我們管理執行緒的 framework。GCD 提供三種工具,分別是 Dispatch QueuesDispatch Sources、和 Operation Queues。我們在本文章中只會討論 dispatch queues。對於 GCD 來說,我們要並行處理的程式碼稱為任務(task)。我們將任務提交(submit)到 dispatch queue,GCD 會從 dispatch queue 中一個一個地取出任務,並且從 thread pool 中挑選一個空閒的(idle) thread 來執行任務,如下圖。取出的順序是先進先出(first-in, first out, FIFO)。

Dispatch Queues
Dispatch Queues

Serial Dispatch Queues

Serial queues 是一個一個地執行任務。只有當目前的任務執行完畢後,才會執行下一個任務,如下圖。

Serial Dispatch Queues
Serial Dispatch Queues

因為 serial queue 在同一個時間只會執行一個任務,所以任務與任務間就不會有資源(resources)的 race conditions。所以,當你想要避免任務間有 race condition,你可以用 serial queue。

讓我們來看看如何使用 serial queues。在下面的程式碼中,我們建立一個 DispatchQueue 的實例(instance),而參數 label 是指這個 dispatch queue 的名稱。這樣就建立好一個 serial queue 了。然後,呼叫 DispatchQueue.async() 來提交(submit)一個任務。

let serialQueue = DispatchQueue(label: "com.waynestalk.serial")
print("start")
serialQueue.async {
    print("Task 1")
}
serialQueue.async {
    print("Task 2")
}
print("end")

上面的程式碼會輸出以下:

start
end
Task 1
Task 2

DispatchQueue.async() 將任務提交給 serial queue 後就馬上返回,任務之後才會被安排執行。這就是以非同步的方式來執行任務。

DispatchQueue 也提供同步的方式來執行任務,如下方的程式碼。

let serialQueue = DispatchQueue(label: "com.waynestalk.serial")
print("start")
serialQueue.sync {
    print("Task 1")
}
serialQueue.sync {
    print("Task 2")
}
print("end")

上方的程式碼會輸出以下:

start
Task 1
Task 2
end

我們可以看到 DispatchQueue.sync() 會 block 目前的執行緒,等待 serial queue 執行完任務後才會返回。

聰明的讀者看到這裡應該會發現到,上面用 DispatchQueue.sync() 執行任務的程式碼根本不太能算是並行處理,因為它必須要等待任務完成才能繼續執行。所以,我們大多都是使用 DispatchQueue.async()。當你必須要使用 DispatchQueue.sync() 時,或許可以先想想是否有辦法使用 DispatchQueue.async() 來達成你的目的。

Concurrent Dispatch Queues

Concurrent queues 讓你可以在一個時間同時執行數個任務。它執行任務的順序也是先進先出。不過它開始執行一個任務後,不需要等待任務完成,就馬上執行下一個任務。那它最多可以同時執行多少任務呢?這個數值是定義在系統裡面的,我們無法從程式中去變更,而且其實你也不需要去關心這個數值。

Concurrent Dispatch Queues
Concurrent Dispatch Queues

我們一樣用 DispatchQueue 來建立一個 concurrent queue,但是在參數 attributes 那邊要傳入 .concurrent

let concurrentQueue = DispatchQueue(label: "com.waynestalk.concurrent", attributes: .concurrent)
print("start")
concurrentQueue.async {
    sleep(2)
    print("Task 1")
}
concurrentQueue.async {
    sleep(1)
    print("Task 2")
}
print("end")

上面的程式碼會輸出以下:

start
end
Task 2
Task 1

可以看出 concurrent queue 並行處理 Task 1 和 Task 2。

Concurrent queues 也有 .sync() 方法,它也是會先 block 目前的執行緒,等待任務完成後才返回。

Quality-of-Service (QoS)

一個應用程式可以創建很多的 dispatch queues,所以我們可能會想要某個 dispatch queue 的優先權高於其他的 dispatch queues。DispatchQueue 當然有提供這樣的功能,這稱為 Quality-ofService (QoS)

下面的程式碼顯示如何對一個 dispatch queue 設定 QoS。主要就是在 DispatchQueue 的 qos 參數設定就可以了。

let concurrentQueue = DispatchQueue(label: "com.waynestalk.concurrent", qos: .userInitiated, attributes: .concurrent)
let serialQueue = DispatchQueue(label: "com.waynestalk.serial", qos: .background)

DispatchQoS.QoSClass 定義了六個優先權,如下(優先權順序由高至低):

  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified

Global Dispatch Queues

系統為每個應用程式預設建立了全域的(global)concurrent queues。因此,你應當先考慮使用這些 global concurrent queues,取得方式如下:

let defaultConcurrentQueue = DispatchQueue.global()
let backgroundConcurrentQueue = DispatchQueue.global(qos: .background)

系統只建立 global concurrent queues,如果你要使用 serial queues 的話,你還是必須要自己建立。

Main Dispatch Queue

Main dispatch queue 是一個 global serial dispatch queue,而且它會在 main thread 上執行任務。Main thread 是唯一一個可以更新畫面的 thread。下面的程式碼展示如何在 main dispatch queue 執行任務。

DispatchQueue.main.async {
    print("Task 1")
}

使用 main dispatch queue 的時機還蠻多的,最典型的就是在 background thread 裡取得網路資料,然後在 main thread 裡顯示資料在畫面上,如下:

// in main thread
...

DispatchQueue.global.async {
    // in a background thread
    // get data from internet
    ....

    DispatchQueue.main.async {
        // in main thread
        // update user interface
        ...
    }
}

避免建立過多的 Threads

既然 GCD 會幫我們管理執行緒,那我們為何還要避免建立過多的 threads 呢?這是因為 GCD 會盡量地保證所有的 dispatch queues 都可以正常地運作,所以在一些情況它會建立過多的 threads 來執行 dispatch queues。在 Avoiding Excessive Thread Creation 中有詳細的討論。

Blocking 目前的 Thread

當一個任務執行在一個 concurrent dispatch queue 裡,然後它呼叫某個方法來 block 目前的 thread 時,系統就會建立另一個 thread 來執行其他在 concurrent dispatch queue 裡的任務。當有太多的任務 block 它們的 threads,那就會消耗光應用程式的資源。所以我們應該要盡量避免在任務中呼叫會 block thread 的方法。

建立過多的 Concurrent Dispatch Queues

當我們建立過多的 concurrent dispatch queues 時,也會導致系統建立過多的 threads 來執行任務。當然 serial dispatch queues 也會有影響,但是影響比較小。因為它是一次執行一個任務,所以只需要一個 thread 即可。所以當需要使用 concurrent dispatch queues,在建立新的之前,我們應當要先考慮使用 global dispatch queues。

如果你非常希望建立自己的 concurrent dispatch queues,或是你必須要建立 serial dispatch queues 時,你可以將你新建立的 dispatch queue 關聯到 global dispatch queues,如下:

let globalQueue = DispatchQueue.global()
let queue = DispatchQueue(label: "com.waynestalk.serial", target: globalQueue)
queue.async {
    print("Task is being executed on a global queue")
}

參數 target 的詳細說明可以參考 DispatchQueue.setTarget()

結論

DispatchQueue 非常地易於使用。當你需要並行處理某段程式碼時,你只需要呼叫 DispatchQueue.async() 方法即可。不需要建立新的執行緒,也不需要管理眾多的執行緒。GCD 會非常有效率地處理這些事情。我們可以更專注於邏輯上的開發。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Florinel Gorgan on Unsplash
Read More

如何製作一個 XCFramework

XCFramework 讓你可以將 iPhone、iPhone 模擬器等多的不同平台的二進位碼打包到一個可發佈的 .xcframework 檔。你只需要為你的 Framework 產生出一個 .xcframework 檔,就可以支援多種平台。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More