Swift

了解 Swift Concurrency 如何限制 thread 上限 避免發生 thread explosion

Swift Concurrency 會限制我們使用比 CPU core 數量更多的 thread,來防止 thread explosion 發生。在這篇文章中,Kah Seng 會帶我們做幾個測試,來看看當中的操作,並試試是否可以欺騙系統,來建立超出 CPU core 數量的 thread。
了解 Swift Concurrency 如何限制 thread 上限 避免發生 thread explosion
Photo by Maury Page on Unsplash
了解 Swift Concurrency 如何限制 thread 上限 避免發生 thread explosion
Photo by Maury Page on Unsplash
In: Swift
本篇原文(標題:How Does Swift Concurrency Prevents Thread Explosions?)刊登於 Swift Senpai,由 Lee Kah Seng 所著,並授權翻譯及轉載。

幾個星期前,我看到一篇 Wojciech Kulik 所寫的文章,當中提到 Swift Concurrency 框架中的一些陷阱。在其中一部分,Wojciech 簡單提到了 thread explosion,以及 Swift Concurrency 如何限制我們使用比 CPU core 數量更多的 thread,來防止 thread explosion 發生。

看完之後我不禁思考,這是真的嗎?這是如何操作的呢?我們是否可以欺騙系統,來建立超出 CPU core 數量的 thread 呢?

我們會在這篇文章中找到答案。讓我們開始吧!

了解 Thread Explosion 💥

Thread explosion 是什麼呢?當系統同時運行大量 thread,而影響效能和導致 memory overhead,就是 thread explosion。

沒有一個明確數字指多少 thread 才算是太多。一般來說,我們可以參考這段 WWDC 影片的範例,當中系統運行中的 thread 數量是其 CPU core 的 16 倍,這就算是 thread explosion。

因為 Grand Central Dispatch (GCD) 沒有內置機制來防止 thread explosion,我們可以簡單利用 dispatch queue 來引發 thread explosion。讓我們來看看以下程式碼:

final class HeavyWork {
    static func dispatchGlobal(seconds: UInt32) {
        DispatchQueue.global(qos: .background).async {
            sleep(seconds)
        }
    }
}

// Execution:
for _ in 1...150 {
    HeavyWork.dispatchGlobal(seconds: 3)
}

執行以上程式碼後會產生共 150 個 thread,因而導致 thread explosion。我們可以暫停執行程序,並查看 debug navigator 來確認 thread explosion 的情況。

Xcode debug navigator that shows thread explosion when using GCD

現在我們知道如何觸發 thread explosion,接下來讓我們試著使用 Swift Concurrency 執行相同的程式碼,看看會發生什麼事。

Swift Concurrency 如何管理 Thread

Swift Concurrency 有 3 個級別的 task priority,分別是 userInitiatedutility、和 background,其中 userInitiated 的優先級別最高,其次是 utility,優先級別最低的是 background。接下來,讓我們相應地更新 HeavyWork 類別:

class HeavyWork {
    
    static func runUserInitiatedTask(seconds: UInt32) {
        Task(priority: .userInitiated) {
            print("🥸 userInitiated: \(Date())")
            sleep(seconds)
        }
    }
    
    static func runUtilityTask(seconds: UInt32) {
        Task(priority: .utility) {
            print("☕️ utility: \(Date())")
            sleep(seconds)
        }
    }
    
    static func runBackgroundTask(seconds: UInt32) {
        Task(priority: .background) {
            print("⬇️ background: \(Date())")
            sleep(seconds)
        }
    }
}

每次創建任務時,我們都會印出創建時間,這個資料可以讓我們可視化 (visualize) 幕後發生的事情。

更新好 HeavyWork 類別之後,讓我們開始進行第一個測試吧!

測試 1:建立優先度相同的 Task

這個測試基本上與前文的 dispatch queue 範例一樣,但這次我們不是使用 GCD 來建立一個 thread,而是使用 Swift Concurrency 的 Task

// Test 1: Creating Tasks with Same Priority Level
for _ in 1...150 {
    HeavyWork.runUserInitiatedTask(seconds: 3)
}

以下是 Xcode 控制台的 log:

Swift concurrency running maximum 6 threads at a time

我們可以從任務的創建時間看到,當 thread 的數目達到 6,系統就會停止建立 thread,這與我的 6-core iPhone 12 的 CPU core 數量一樣。只有在系統完成其中一個執行中的任務後,才會繼續創建任務。也就是說,最多只能有 6 個 thread 在同一時間運行。

備註:
無論選擇了什麼設備,iOS 模擬器都會將 thread 的上限設置為 1。因此,請使用真實設備來運行上述測試,以獲得更準確的結果。

讓我們暫停執行操作,來看看到底在幕後發生了什麼事吧!

Swift Concurrency tasks with 'userInitiated' priority running on a concurrent queue

我們從上圖可以看到,任務都是由 “com.apple.root.user-initiated-qos.cooperative” concurrent queue 控制的。

因此我們可以確定,Swift Concurrency 就是利用一個專用的 concurrent queue 來限制 thread 的數量,讓它不過多於 CPU core,來防止 thread explosion:

測試 2:同時按優先級別高至低創建任務

接下來,讓我們試著添加不同優先級別的任務:

// Test 2: Creating Tasks from High to Low Priority Level All at Once
for _ in 1...30 {
    HeavyWork.runUserInitiatedTask(seconds: 3)
}

for _ in 1...30 {
    HeavyWork.runUtilityTask(seconds: 3)
}

for _ in 1...30 {
    HeavyWork.runBackgroundTask(seconds: 3)
}

在上面的程式碼中,我們會先建立優先級別最高的任務 (userInitiated),然後才建立 utilitybackground。根據我們在上一個測試得出的結論,應該會看到 3 個 queue,每個 queue 有 6 個 thread 在同時運行,也就是說應該一共會有 18 個 thread。但事實並非如此,讓我們看看下面的截圖:

Swift Concurrency tasks distribution when starting from high to low priority level all at once

如你所見,當優先級較高的 queue 隊列 (userInitiated) 飽和時,utilitybackground 的 queue 的上限會被限制為 1。也就是說,我們在這個測試中最多可以有 8 個 thread。

這個發現太有趣了!原來優先級別較高的 queue 飽和時,系統會抑制其他優先級別較低的 queue,不讓 thread 的數量繼續增加。

那如果我們把優先級別的順序倒轉,又會發生什麼事呢?

測試 3:同時按優先級別低至高創建任務

首先,讓我們更新程式碼:

// Test 3: Creating Tasks from Low to High Priority Level All at Once
for _ in 1...30 {
    HeavyWork.runBackgroundTask(seconds: 3)
}

for _ in 1...30 {
    HeavyWork.runUtilityTask(seconds: 3)
}

for _ in 1...30 {
    HeavyWork.runUserInitiatedTask(seconds: 3)
}

以下就是測試結果:

Swift Concurrency tasks distribution when starting from low to high priority level all at once

結果與「測試 2」一模一樣。

似乎系統十分聰明,即使我們先創建優先級別較低的任務,系統都會先執行優先級別較高的任務。而且,系統還是會限制我們最多只可以同時執行 8 個 thread,因此不會造成 thread explosion。Apple 做得不錯喔!

測試 4:同時按優先級別從低至高創建任務 並在中間添加一個 Break

在現實生活中,我們不太可能會同時啟動一堆優先級別不同的任務。因此,讓我們構建一個更貼近現實的情況,就是在每個 for loop 之間添加一個 break。在這個測試中,我們還是會按優先級別從低至高創建任務。

// Test 4: Creating Tasks from Low to High Priority Level with Break in Between
for _ in 1...30 {
    HeavyWork.runBackgroundTask(seconds: 3)
}

sleep(3)
print("⏰ 1st break...")

for _ in 1...30 {
    HeavyWork.runUtilityTask(seconds: 3)
}

sleep(3)
print("⏰ 2nd break...")

for _ in 1...30 {
    HeavyWork.runUserInitiatedTask(seconds: 3)
}

我們得到的結果很有趣。

Swift Concurrency tasks distribution when starting from low to high priority level with breaks in between

如你所見,在第 2 個 break 之後,3 個 queue 都在運行多個 thread。也就是說如果我們先啟動優先級別較低的 queue,並讓它運行一段時間,優先級別較高的 queue 就不會限制優先級別較低的 queue 的數量。

我執行了這個測試幾次,thread 的上限可能會有所不同,但都大約等於 CPU core 的 3 倍。

這算不算是 thread explosion 呢?

我覺得不算是,畢竟 3 倍遠遠不及前文提過的 16 倍。我認為 Apple 其實是故意允許這種情況的,從而在執行效率和 multi-thread overhead 之間取得更好的平衡。如果你有其他想法,歡迎在 Twitter 告訴我,我十分希望了解大家的想法。

總結

Swift Concurrency 真的能防止 thread explosion 發生,但我們不得不承認,如果 userInitiated queue 不斷處於飽和的情況,它其實會引致很嚴重的瓶頸。我將會繼續深入探討這個議題,敬請期待。

從「測試 4」的結果可見,其實我們平常應該多點使用 backgroundutility queue,只在必要時使用 userInitiated queue。

如果有需要,你可以從 GitHub 下載文章的範例程式碼。

如果你喜歡這篇文章,請不要錯過我其他有關 Swift Concurrency 的文章。你也可以在 Twitter 上 follow 我,並訂閱我的 newsletter,以免錯過我之後發表的文章。

謝謝你的閱讀。

本篇原文(標題:How Does Swift Concurrency Prevents Thread Explosions?)刊登於 Swift Senpai,由 Lee Kah Seng 所著,並授權翻譯及轉載。
作者簡介:Lee Kah Seng,馬來西亞人,2011年成為 iOS 開發者,喜歡 Swift、音樂、和日本動畫,是一名兼職「背包客」。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
如何把 Swift DocC 文檔託管到 Web Server 或 GitHub
Xcode

如何把 Swift DocC 文檔託管到 Web Server 或 GitHub

Apple 在 Xcode 13 推出了文檔編譯工具 Swift DocC,讓開發者可以為專案創建漂亮的交互式文檔,我們還可以將把文檔託管在網站上。在這篇文章中,我會簡單介紹 Swift DocC,並教大家把程式碼文檔發佈到自己的網頁或 GitHub,與更多讀者共享文檔。
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。