通过前面的例子可以看到,task group 用来表达具有依赖关系的异步任务非常方便。但无论是 task group 对作用域的严格控制,还是在其中创建任务的方法,如果用它来表达所有的异步任务,不免有些过于繁琐。一个最明显的例子就是,我们无法在同步函数中创建 task group,就更别谈什么执行异步任务了。因此,有时候你会觉得,还是简简单单用个回调函数更好。
为了解决这个问题,Swift 结构化并发模型中还定义了另外一种叫 unstructured task 的任务。还是基于之前的例子,我们直接通过代码来理解它的用法。
首先,给 Meal
添加一个 eat
方法:
struct Meal {
func eat() {
print("Yum!")
}
}
其次,再定义一个吃晚餐的全局函数 eat
,它接受一个可以制作出晚餐的任务作为参数:
func eat(_ task: Task<Meal, Error>) async throws {
let meal = try await task.value
meal.eat()
}
这里,Task
就是刚才提到的 unstructured task,它的两个泛形参数分别表示这个任务正常完成以及出错时的返回值。在 eat
的实现里,我们可以通过 Task
的 value
属性得到这个任务完成后的返回值,也就是 Meal
对象。之后,就可以调用 meal.eat
开吃了。
实际上,
Task
对象可以理解为是用来探查任务自身的一个句柄,除了返回值之外,我们还可以用它查询任务的取消状态以及取消任务。
在异步环境中使用 Task
那么,该如何创建一个返回 Meal
的任务并使用它呢?先来看在异步环境中的情况:
@main
struct MyApp {
static func main() async {
let meal = Task(priority: .userInitiated) {
() -> Meal in
try await makeDinnerWithThrowingTaskGroup()
}
try? await eat(meal)
}
}
Task
有两个版本的 init
方法:
public init(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async -> Success)
public init(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success)
其中:
- 第一个参数是
TaskPriority
类型的对象,表示任务的优先级; - 第二个参数是个异步 closure,表示这个任务要执行的代码,这两个
init
方法的区别,主要就是 closure 是否会抛出异常。这个 closure 没有参数,但允许让我们指定一个返回值,表示任务成功执行后的结果。在我们的例子中,当然就是Meal
了。至于那个@Sendable
修饰,我们暂时先忽略它就好了;
在这个 closure 的实现里,我们直接调用了上一节定义的 makeDinnerWithThrowingTaskGroup()
函数。这样,一个可以创造出晚餐的任务就定义好了。Task
对象一旦创建完,任务就开始执行了。之后,当我们把它传递给全局函数 eat
,此时的 meal
就相当于这个任务的句柄,在 eat
的实现里,就可以等待 meal.value
来获取任务的返回值。然后,我们就能在控制台看到这样的结果了:
Chopping vegetables
Marinate meat
Preheat oven.
Cook 300 seconds.
Yum!
在同步环境中执行任务
接下来,回到这一节开始的问题,如何在同步环境中执行异步函数 eat
呢?我们注意到,Task.init
并不是异步的,因此,把异步函数用 Task
包一下就好了。像这样:
@main
struct MyApp {
static func main() {
Task(priority: .userInitiated) {
let meal = Task { () -> Meal in
try await makeDinnerWithThrowingTaskGroup()
}
try await eat(meal)
exit(EXIT_SUCCESS)
}
dispatchMain()
}
}
当然,执行的结果是一样的,我们就不重复了。
Unstructured task 的嵌套
这一节最后,我们再回过头看看刚才写的同步 main
函数。我们使用了一个嵌套的 Task
结构,外面的叫做父任务,里面的叫做子任务。
当在一个 Task
中创建子任务的时候,子任务会继承父任务的优先级,因此,即便我们没有指定内层任务的优先级,它的优先级也是 .userInitiated
。我们可以把 main
改成这样来观察:
@main
struct MyApp {
static func main() {
Task(priority: .userInitiated) {
let meal = Task { () -> Meal in
print(Task.currentPriority == .userInitiated) // True
return try await makeDinnerWithThrowingTaskGroup()
}
try await eat(meal)
exit(EXIT_SUCCESS)
}
dispatchMain()
}
}
这里,Task.currentPriority
用来获取当前正在执行的任务的优先级。我们应该可以在控制台看到 True
。
What's next?
至此,关于 unstructured task 的基础知识,我们就说的差不多了。实际上,除了这种较为独立的任务之外,Swift 结构化并发模型中还提供了一种任务,叫做 detached task。下一节,我们来聊聊它,并补充一些关于独立任务还没提到的内容。