Swift 并發(fā)中的 Yielding 和 Debouncing
前言
本篇文章的主題是 任務(wù)讓步(Task Yielding) 和 防抖(Debouncing)。Swift 并發(fā)為我們提供了兩個簡單但非常強大的函數(shù):yield 和 sleep。今天我們就來看看它們的用法,以及在什么場景下應(yīng)該使用它們。
什么是任務(wù)防抖(Debouncing)?
想象一下,你正在開發(fā)一個搜索功能,用戶每輸入一個字符,程序就會去一個龐大的數(shù)據(jù)集里查找匹配的結(jié)果。如果不加控制,每次鍵入都會觸發(fā)新的搜索任務(wù),可能會導(dǎo)致多個任務(wù)同時執(zhí)行,影響性能甚至引發(fā)競爭條件(Race Condition)。
比如,下面這個 SwiftUI 代碼:
@MainActor @Observablefinalclass Store {
private(set) var results: [HKCorrelation] = []
privatelet store = HKHealthStore()
func search(matching query: String) async {
// 執(zhí)行復(fù)雜的搜索任務(wù)
}
}
struct ContentView: View {
@Stateprivatevar store = Store()
@Stateprivatevar query = ""
var body: some View {
NavigationStack {
List(store.results, id: \.uuid) { result in
Text(verbatim: result.endDate.formatted())
}
.searchable(text: $query)
.task(id: query) {
await store.search(matching: query)
}
}
}
}
在這個代碼里,每次用戶輸入新字符,都會創(chuàng)建一個新的任務(wù)去執(zhí)行搜索。而 Swift 并發(fā)采用 協(xié)作式取消機制(Cooperative Cancellation),也就是說,它不會直接強行終止任務(wù),而是提供一個“取消標記”,任務(wù)需要自己檢查并響應(yīng)取消請求。因此,這種寫法可能會導(dǎo)致多個搜索任務(wù)并行運行,消耗不必要的計算資源。
為了解決這個問題,我們可以用 防抖(Debouncing) 技術(shù)。
如何用 sleep 實現(xiàn)防抖?
防抖的思路很簡單:
- 用戶輸入時,我們 等待一小段時間,看看用戶是否繼續(xù)輸入。
- 如果輸入仍然在變化,我們就 繼續(xù)等待,而不是立即啟動搜索。
- 只有當(dāng)輸入 穩(wěn)定 一定時間后,才觸發(fā)搜索任務(wù)。
換句話說,如果用戶輸入 "apple",我們希望忽略 "a", "ap", "app", "appl",只在最終輸入 "apple" 后再進行搜索。
在 Swift 并發(fā)中,我們可以用 Task.sleep 來實現(xiàn)這個效果:
struct ContentView: View {
@Stateprivatevar store = Store()
@Stateprivatevar query = ""
var body: some View {
NavigationStack {
List(store.results, id: \.uuid) { result in
Text(verbatim: result.endDate.formatted())
}
.searchable(text: $query)
.task(id: query) {
do {
try await Task.sleep(for: .seconds(1)) // 等待 1 秒
await store.search(matching: query) // 執(zhí)行搜索
} catch {
// 任務(wù)可能因為新輸入被取消
}
}
}
}
}
為什么這樣就能實現(xiàn)防抖?
- Task.sleep(for: .seconds(1)) 讓當(dāng)前任務(wù)暫停 1 秒。
- 如果用戶在 1 秒內(nèi)繼續(xù)輸入,之前的任務(wù)會被取消,新任務(wù)重新計時。
- 只有 用戶停止輸入超過 1 秒,才會觸發(fā)真正的搜索任務(wù)。
效果: 這樣可以避免在輸入過程中反復(fù)觸發(fā)搜索,減少不必要的計算量。
什么是任務(wù)讓步(Task Yielding)?
除了防抖,Swift 并發(fā)還提供了 任務(wù)讓步(Task Yielding),讓你在執(zhí)行長時間任務(wù)時,主動把線程讓出來,讓其他任務(wù)有機會運行。
想象一個場景:
你需要解析一批巨大的 JSON 文件,并將數(shù)據(jù)保存到磁盤。這個過程可能會運行很久,占用線程資源。如果你在主線程或并發(fā)線程池(Cooperative Thread Pool)上運行這種任務(wù),會 阻塞其他任務(wù)的執(zhí)行,導(dǎo)致性能問題。
比如,下面是一個解析 JSON 文件的代碼:
struct Item: Decodable {
// 解析 JSON 的結(jié)構(gòu)
}
struct DataHandler {
func process(json files: [Data]) async throws -> [Item] {
let decoder = JSONDecoder()
var result: [Item] = []
for file in files {
let items = try decoder.decode([Item].self, from: file)
result.append(contentsOf: items)
}
return result
}
}
這個 process
函數(shù)會遍歷所有 JSON 文件,并解析它們。但問題是:
- 解析 JSON 是一個 同步操作,它不會自動釋放 CPU 資源。
- 如果 JSON 文件很大,整個解析過程可能會 占用線程很長時間,導(dǎo)致其他任務(wù)被阻塞。
如何用 yield 讓出線程?
為了解決這個問題,我們可以 在每次解析完一個 JSON 文件后,讓出線程,讓其他任務(wù)有機會執(zhí)行:
struct DataHandler {
func process(json files: [Data]) async throws -> [Item] {
let decoder = JSONDecoder()
var result: [Item] = []
for file in files {
let items = try decoder.decode([Item].self, from: file)
result.append(contentsOf: items)
await Task.yield() // 讓出線程,讓其他任務(wù)有機會執(zhí)行
}
return result
}
}
任務(wù)讓步的好處:
- await Task.yield() 會讓當(dāng)前任務(wù) 暫停一下,讓其他等待中的任務(wù)有機會執(zhí)行。
- 之后,系統(tǒng)會恢復(fù)這個任務(wù)的執(zhí)行,繼續(xù)處理下一個 JSON 文件。
- 這樣可以 更公平地分配 CPU 資源,防止某個任務(wù)獨占線程。
什么時候需要 yield?
通常來說,如果你的代碼已經(jīng)是 異步的(async/await),系統(tǒng)會自動在 await 語句處讓出線程。所以 **大部分情況下,你不需要手動 yield**。
但是,當(dāng)你處理 非異步 API(比如 JSON 解析、圖片處理、大量計算等)時,手動 yield 可能會提升性能。
總結(jié)
- 防抖(Debouncing)
a.適用于 用戶頻繁輸入的場景,如搜索框、按鈕點擊等。
b.通過 Task.sleep(for:) 實現(xiàn),等輸入穩(wěn)定后再執(zhí)行任務(wù)。
c.避免頻繁創(chuàng)建任務(wù),提高性能。
- 任務(wù)讓步(Task Yielding)
a.適用于 長時間運行的計算密集型任務(wù),如解析 JSON、圖片處理等。
b.通過 Task.yield() 讓出 CPU,避免線程被長時間占用。
c.讓其他任務(wù)有機會執(zhí)行,提高系統(tǒng)響應(yīng)速度。
這兩個技巧雖然簡單,但在實際開發(fā)中非常有用,可以幫助你更高效地利用 Swift 并發(fā),讓你的應(yīng)用運行得更流暢!