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