實戰指南 | Swift 并發中的任務取消機制
前言
Swift 并發提供了一種協作式取消(cooperative cancellation) 機制,來讓任務在需要時自己退出。簡單來說,Swift 不會強行終止你的任務,但它會告訴你任務已經被標記為取消,至于你要不要停下來,那是你自己的決定。
這篇文章會講清楚任務取消的原理、如何正確使用它,以及如何寫出高效又優雅的代碼。
什么是協作式取消?
協作式取消的核心思想是:
- 調用方(比如 SwiftUI)沒法直接終止任務,只能“標記”為取消。
- 任務本身需要定期檢查這個標記,并決定要不要提前終止。
- 你可以選擇直接返回、提供部分結果,或者繼續執行,全看你的業務邏輯。
簡單來說,Swift 只是給你一個“信號”——“嘿,這個任務已經沒用了,看看你要不要停下來”。
如何用 Task API 處理任務取消
來看個例子,這是一個 SwiftUI 界面,用戶輸入搜索內容時,會觸發異步搜索。
struct ContentView: View {
@Stateprivatevar store = Store()
@Stateprivatevar query = ""
var body: some View {
NavigationStack {
List(store.results, id: \.self) { result in
Text(verbatim: result)
}
.searchable(text: $query)
.task(id: query) {
await store.search(matching: query)
}
}
}
}
這里最關鍵的是 task(id: query) 這行代碼:
- 當
query
變化時,SwiftUI 會啟動一個新的搜索任務。 - 同時,它會標記上一個任務為“已取消”,但不會立刻終止它。
- 如果舊任務里沒有檢查取消狀態,它可能仍然會跑完所有邏輯。
這意味著,如果用戶輸入了很多字符,可能會同時存在多個搜索任務,這就是為什么我們要手動處理取消邏輯。
在異步方法中正確處理取消
假設 Store 負責查詢數據,我們的 search(matching:) 方法如下:
import HealthKit
@MainActor @Observablefinalclass Store {
private(set) var results: [HKCorrelation] = []
privatelet store = HKHealthStore()
func search(matching query: String) async {
let foodQuery = HKSampleQueryDescriptor(
predicates: [.correlation(type: .init(.food))],
sortDescriptors: []
)
do {
let food = try await foodQuery.result(for: store)
tryTask.checkCancellation() // 檢查任務是否已取消
results = food.filter { food in
let title = food.metadata?["title"] as? String ?? ""
return title.localizedStandardContains(query)
}
} catch {
results = []
}
}
}
這里有個關鍵點:**Task.checkCancellation()**。
- 這個方法會拋出一個錯誤,如果任務已經被取消,它就會立刻停止執行,后續的代碼不會再運行。
- 這樣可以避免執行一些不必要的邏輯,比如過濾數據、更新 UI 等。
- 如果任務被取消,我們直接把 results 置空,這樣用戶不會看到過時的搜索結果。
在多個步驟中檢查取消狀態
如果你的異步代碼有多個步驟,比如先獲取數據、然后再做一些處理,那你可能需要在多個關鍵點檢查任務是否已取消,否則即使任務已經無效了,它可能還會跑完整個流程。
import HealthKit
@MainActor @Observablefinalclass Store {
private(set) var results: [HKCorrelation] = []
privatelet store = HKHealthStore()
func search(matching query: String) async {
let foodQuery = HKSampleQueryDescriptor(
predicates: [.correlation(type: .init(.food))],
sortDescriptors: []
)
do {
let food = try await foodQuery.result(for: store)
tryTask.checkCancellation() // 第一次取消檢查
// 假設這里有額外的數據處理
tryTask.checkCancellation() // 第二次取消檢查
results = food.filter { food in
let title = food.metadata?["title"] as? String ?? ""
return title.localizedStandardContains(query)
}
} catch {
results = []
}
}
}
為什么要多次檢查?
- 如果 foodQuery 運行了一段時間,任務被取消了,我們希望盡早停下來,而不是等所有代碼都跑完。
- 某些任務可能是分步執行的,比如先獲取原始數據,再處理數據。如果第一步完成了,但任務已經取消了,我們就沒必要繼續處理數據。
用 isCancelled 進行檢查
除了 Task.checkCancellation() 之外,Swift 還提供了 Task.isCancelled 這個屬性,它是一個布爾值,你可以用它更靈活地處理任務取消:
actor SearchService {
privatevar cachedResults: [HKCorrelation] = []
privatelet store = HKHealthStore()
func search(matching query: String) async throws -> [HKCorrelation] {
guard !Task.isCancelled else {
return cachedResults // 任務取消了,直接返回緩存
}
let foodQuery = HKSampleQueryDescriptor(
predicates: [.correlation(type: .init(.food))],
sortDescriptors: []
)
let food = try await foodQuery.result(for: store)
guard !Task.isCancelled else {
return cachedResults // 任務取消了,避免不必要的計算
}
cachedResults = food.filter { food in
let title = food.metadata?["title"] as? String ?? ""
return title.localizedStandardContains(query)
}
return cachedResults
}
}
兩種方式的區別:
- Task.checkCancellation():如果任務已取消,直接拋出錯誤,代碼不再繼續執行。
- Task.isCancelled:任務是否繼續執行,由你自己決定,比如可以提前返回緩存數據,而不是直接終止。
手動取消任務
通常情況下,Swift 會幫你管理任務的取消,但如果你想手動創建和取消任務,也可以用 Task:
struct ExampleView: View {
@Stateprivatevar store = Store()
@Stateprivatevar task: Task<Void, Never>?
var body: some View {
VStack {
Button("開始任務") {
task = Task {
await store.fetch()
}
}
Button("取消任務") {
task?.cancel()
}
}
}
}
這里 task?.cancel() 只會標記任務為取消,但不會真的終止它,所以你仍然需要在 fetch() 里檢查 Task.isCancelled 或 Task.checkCancellation()。
總結
- Swift 不會自動終止任務,只會標記它為取消。
- 用 Task.checkCancellation() 可以立即終止任務,防止執行不必要的邏輯。
- 用 Task.isCancelled 可以更靈活地決定如何處理取消。
- 如果任務有多個異步步驟,應該在關鍵點多次檢查取消狀態。
- 手動創建的任務可以用 .cancel() 取消,但仍然需要手動檢查取消狀態。
學會這些,你的 Swift 并發代碼就能更高效、更優雅地處理任務取消,讓用戶體驗更流暢!