Swift 中的 Task 是 WWDC 2021 引入的并發(fā)框架的一部分。任務允許我們從非并發(fā)方法創(chuàng)建并發(fā)環(huán)境,使用 async/await 調(diào)用方法。
第一次處理任務時,您可能會認識到調(diào)度隊列(dispatch queue)和任務(tasks)之間的相識程度。兩者都允許在具有特定優(yōu)先級的不同線程上分派工作。然而,任務通過消除冗長的調(diào)度隊列代碼,使我們的生活變得相當不同且更輕松。
您可以在我的文章 Swift 中的async/await了解有關(guān) async/await 的更多信息。
如何創(chuàng)建然后運行一個 Task
在 Swift 中創(chuàng)建一個basicTask如下所示:
let basicTask = Task {
return "This is the result of the task"
}
如您所見,我們保留了對返回字符串值的 basicTask 的引用。我們可以使用引用來讀出結(jié)果值:
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
// Prints: This is the result of the task
此示例返回一個字符串,但也可能引發(fā)錯誤:
let basicTask = Task {
// .. 做一些工作 ..
throw ExampleError.somethingIsWrong
}
do {
print(try await basicTask.value)
} catch {
print("Basic task failed with error: \(error)")
}
// Prints: Basic task failed with error: somethingIsWrong
換句話說,您可以使用任務來產(chǎn)生值和錯誤。
如何運行任務
好吧,上面的例子已經(jīng)給出了本節(jié)的答案。任務在創(chuàng)建后會立即運行,不需要顯式啟動。重要的是要了解需要執(zhí)行的工作是在任務創(chuàng)建后直接執(zhí)行的,因為它告訴您僅在允許任務內(nèi)工作開始時才會創(chuàng)建它。
在任務中執(zhí)行異步方法
除了同步返回值或拋出錯誤外,任務還可以執(zhí)行異步方法。我們需要一個任務來在不支持并發(fā)的函數(shù)中執(zhí)行任何異步方法。您可能已經(jīng)熟悉以下錯誤:

不支持并發(fā)的函數(shù)中的“async”調(diào)用是 Swift 中的常見錯誤。
在此示例中,executeTask 方法是另一個任務的簡單包裝器:
func executeTask() async {
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
}
我們可以通過在一個新任務中調(diào)用executeTask()方法來解決上述錯誤。
var body: some View {
Text("Hello, world!")
.padding()
.onAppear {
Task {
await executeTask()
}
}
}
func executeTask() async {
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
}
該任務創(chuàng)建了一個并發(fā)支持環(huán)境,我們可以在其中調(diào)用異步方法 executeTask()。有趣的是,即使我們沒有在 onappear 方法中保留對已創(chuàng)建任務的引用,我們的代碼也會執(zhí)行,這里來到我下一節(jié)要說明的內(nèi)容:取消任務。
處理取消
在想到處理任務取消時,您可能會驚訝地看到您的任務正在執(zhí)行,即使您沒有保留對它的引用。 Combine 中的發(fā)布者訂閱要求我們保持強引用以確保發(fā)出值。與 Combine 相比,您可能希望在釋放所有引用后也取消任務。
但是,Task的工作方式不同,因為無論您是否保留引用,它們都會運行。保留引用的唯一原因是讓自己能夠等待結(jié)果或取消任務。
取消一個任務
為了向您解釋任務取消是如何工作的,我們將使用一個加載圖像的新代碼示例:
struct ContentView: View {
@State var image: UIImage?
var body: some View {
VStack {
if let image = image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}.onAppear {
Task {
do {
image = try await fetchImage()
} catch {
print("Image loading failed: \(error)")
}
}
}
}
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
return try await imageTask.value
}
}
上面的代碼例子獲取了一張隨機的圖片,如果請求成功,就會相應地顯示出來。
為了這個演示,我們可以在imageTask創(chuàng)建后立即取消它:
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
return try await imageTask.value
}
上面的取消調(diào)用將會阻止請求,因為 URLSession 實現(xiàn)在執(zhí)行之前會執(zhí)行取消檢查。因此,上面的代碼示例打印出以下內(nèi)容:
Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"
如您所見,我們的打印語句仍在執(zhí)行。這個打印語句是演示了如何使用靜態(tài)取消檢查的兩種方法的其中一種。另一種是通過在檢測到取消時拋出錯誤來停止執(zhí)行當前任務:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
/// 如果任務已被取消,則拋出錯誤。
try Task.checkCancellation()
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
上面的代碼打印結(jié)果為:
Image loading failed: CancellationError()
如您所見,我們的打印語句和網(wǎng)絡請求都沒有被調(diào)用。
我們可以使用的第二種方法給我們一個取消的狀態(tài)。通過使用這種方法,我們允許自己在取消時執(zhí)行任何額外的清理工作:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
guard Task.isCancelled == false else {
// Perform clean up
print("Image request was cancelled")
return nil
}
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
在這種情況下,我們的代碼只打印出取消聲明。
執(zhí)行定期取消檢查對于防止您的代碼做不必要的工作至關(guān)重要。想象一個例子,我們將轉(zhuǎn)換返回的圖像;我們可能應該在整個代碼中添加多個檢查:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
// 在網(wǎng)絡請求之前檢查取消。
try Task.checkCancellation()
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
// 網(wǎng)絡請求后檢查取消,以防止開始我們繁重的圖像操作。
try Task.checkCancellation()
let image = UIImage(data: imageData)
// 由于任務未取消,因此執(zhí)行返回圖像操作。
return image
}
在可以很容易的掌控任務的取消,這使得我們很容易犯錯誤和進行不必要的工作。在執(zhí)行任務時,請保持警惕,確保你的代碼定期檢查取消的狀態(tài)。
設置優(yōu)先級
每個任務都可以有它的優(yōu)先級。我們可以應用的值類似于我們在使用調(diào)度隊列時可以配置的服務質(zhì)量級別。低、中、高優(yōu)先級看起來與操作設置的優(yōu)先級相似。

每個優(yōu)先級都有其目的,并且可以表明一項工作比其他工作更重要。但是不能保證您的任務一定更早執(zhí)行。例如,較低優(yōu)先級的作業(yè)可能已經(jīng)在運行。
配置優(yōu)先級有助于防止低優(yōu)先級任務比更高優(yōu)先級的任務更先執(zhí)行。
用于執(zhí)行的線程
默認情況下,一個任務在一個自動管理的后臺線程上執(zhí)行。通過測試,我發(fā)現(xiàn)默認的優(yōu)先級是25。打印出高優(yōu)先級的原始值,顯示其實相匹配的:
(lldb) p Task.currentPriority
(TaskPriority) $R0 = (rawValue = 25)
(lldb) po TaskPriority.high.rawValue
25
您可以設置斷點來驗證您的方法在哪個線程上運行:

繼續(xù)您的 Swift 并發(fā)之旅
并發(fā)更改不僅僅是async-await,還包括許多您可以在代碼中受益的新功能?,F(xiàn)在您已經(jīng)了解了任務的基礎知識,是時候深入了解其他新的并發(fā)特性了:
- Swift 中的 async/await
- Swift 中的 async let
- Swift 中的 Task
- Swift 中的 Actors 使用以如何及防止數(shù)據(jù)競爭
- Swift 中的 MainActor 使用和主線程調(diào)度
- 理解 Swift Actor 隔離關(guān)鍵字:nonisolated 和 isolated
- Swift 中的 Sendable 和 @Sendable 閉包
- Swift 中的 AsyncThrowingStream 和 AsyncStream
- Swift 中的 AsyncSequence
結(jié)論
Swift 中的Task允許我們創(chuàng)建一個并發(fā)環(huán)境來運行異步方法。取消任務需要明確的檢查,以確保我們不去執(zhí)行任何不必要的工作。通過配置我們?nèi)蝿盏膬?yōu)先級,我們可以管理執(zhí)行的順序。