Swift 中的 Task
是 WWDC 2021 引入的并发框架的一部分。任务允许我们从非并发方法创建并发环境,使用 async/await 调用方法。
第一次处理任务时,您可能会认识到调度队列(dispatch queue)和任务(tasks)之间的相识程度。两者都允许在具有特定优先级的不同线程上分派工作。然而,任务通过消除冗长的调度队列代码,使我们的生活变得相当不同且更轻松。
您可以在我的文章 Swift 中的async/await了解有关 async/await 的更多信息。
如何创建然后运行一个 Task
在 Swift 中创建一个basicTask
如下所示:
let basicTask = Task {
return "This is the result of the task"
}
如您所见,我们保留了对返回字符串值的 basicTask
的引用。我们可以使用引用来读出结果值:
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
// Prints: This is the result of the task
此示例返回一个字符串,但也可能引发错误:
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
换句话说,您可以使用任务来产生值和错误。
如何运行任务
好吧,上面的例子已经给出了本节的答案。任务在创建后会立即运行,不需要显式启动。重要的是要了解需要执行的工作是在任务创建后直接执行的,因为它告诉您仅在允许任务内工作开始时才会创建它。
在任务中执行异步方法
除了同步返回值或抛出错误外,任务还可以执行异步方法。我们需要一个任务来在不支持并发的函数中执行任何异步方法。您可能已经熟悉以下错误:
不支持并发的函数中的“async”调用是 Swift 中的常见错误。
在此示例中,executeTask
方法是另一个任务的简单包装器:
func executeTask() async {
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
}
我们可以通过在一个新任务中调用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)
}
该任务创建了一个并发支持环境,我们可以在其中调用异步方法 executeTask()
。有趣的是,即使我们没有在 onappear
方法中保留对已创建任务的引用,我们的代码也会执行,这里来到我下一节要说明的内容:取消任务。
处理取消
在想到处理任务取消时,您可能会惊讶地看到您的任务正在执行,即使您没有保留对它的引用。 Combine 中的发布者订阅要求我们保持强引用以确保发出值。与 Combine 相比,您可能希望在释放所有引用后也取消任务。
但是,Task的工作方式不同,因为无论您是否保留引用,它们都会运行。保留引用的唯一原因是让自己能够等待结果或取消任务。
取消一个任务
为了向您解释任务取消是如何工作的,我们将使用一个加载图像的新代码示例:
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
创建后立即取消它:
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
}
上面的取消调用将会阻止请求,因为 URLSession
实现在执行之前会执行取消检查。因此,上面的代码示例打印出以下内容:
Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"
如您所见,我们的打印语句仍在执行。这个打印语句是演示了如何使用静态取消检查的两种方法的其中一种。另一种是通过在检测到取消时抛出错误来停止执行当前任务:
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()
上面的代码打印结果为:
Image loading failed: CancellationError()
如您所见,我们的打印语句和网络请求都没有被调用。
我们可以使用的第二种方法给我们一个取消的状态。通过使用这种方法,我们允许自己在取消时执行任何额外的清理工作:
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()
在这种情况下,我们的代码只打印出取消声明。
执行定期取消检查对于防止您的代码做不必要的工作至关重要。想象一个例子,我们将转换返回的图像;我们可能应该在整个代码中添加多个检查:
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)
// 网络请求后检查取消,以防止开始我们繁重的图像操作。
try Task.checkCancellation()
let image = UIImage(data: imageData)
// 由于任务未取消,因此执行返回图像操作。
return image
}
在可以很容易的掌控任务的取消,这使得我们很容易犯错误和进行不必要的工作。在执行任务时,请保持警惕,确保你的代码定期检查取消的状态。
设置优先级
每个任务都可以有它的优先级。我们可以应用的值类似于我们在使用调度队列时可以配置的服务质量级别。低、中、高优先级看起来与操作设置的优先级相似。
每个优先级都有其目的,并且可以表明一项工作比其他工作更重要。但是不能保证您的任务一定更早执行。例如,较低优先级的作业可能已经在运行。
配置优先级有助于防止低优先级任务比更高优先级的任务更先执行。
用于执行的线程
默认情况下,一个任务在一个自动管理的后台线程上执行。通过测试,我发现默认的优先级是25。打印出高优先级的原始值,显示其实相匹配的:
(lldb) p Task.currentPriority
(TaskPriority) $R0 = (rawValue = 25)
(lldb) po TaskPriority.high.rawValue
25
您可以设置断点来验证您的方法在哪个线程上运行:
继续您的 Swift 并发之旅
并发更改不仅仅是async-await,还包括许多您可以在代码中受益的新功能。现在您已经了解了任务的基础知识,是时候深入了解其他新的并发特性了:
- Swift 中的 async/await
- Swift 中的 async let
- Swift 中的 Task
- Swift 中的 Actors 使用以如何及防止数据竞争
- Swift 中的 MainActor 使用和主线程调度
- 理解 Swift Actor 隔离关键字:nonisolated 和 isolated
- Swift 中的 Sendable 和 @Sendable 闭包
- Swift 中的 AsyncThrowingStream 和 AsyncStream
- Swift 中的 AsyncSequence
结论
Swift 中的Task
允许我们创建一个并发环境来运行异步方法。取消任务需要明确的检查,以确保我们不去执行任何不必要的工作。通过配置我们任务的优先级,我们可以管理执行的顺序。
转自 Tasks in Swift explained with code examples