Detached task 是 Swift 结构化并发模型中另外一种独立运行的任务。不过,别被 detached 这个词迷惑,这种任务并不能像守护进程一样脱离开我们的主程序单独运行。这个 detached 是相对于结构化并发模型的作用域的。
我们来看个例子:
@main
struct MyApp {
static func main() {
Task(priority: .userInitiated) {
Task.detached {
print("Start playing BGM.")
}
let meal = Task { () -> Meal in
return try await makeDinnerWithThrowingTaskGroup()
}
try await eat(meal)
Task.detached {
print("Stop playing BGM.")
}
exit(EXIT_SUCCESS)
}
dispatchMain()
}
}
基于上一节的例子,我们在开始准备晚餐之前,用 Task.detached
创建了一个播放 BGM 的任务。然后,在 eat
函数返回之后,我们停止播放 BGM。
播放 BGM 和准备晚餐这两个任务的执行顺序是不确定的,它们是两个并行的任务。
执行一下,就会在控制台看到类似这样的结果:
Start playing BGM.
Chopping vegetables
Preheat oven.
Marinate meat
Cook 300 seconds.
Yum!
Stop playing BGM.
Detached 和 unstructured task 的区别
那么 detached task 和上一节的 unstructured task 有什么区别呢?
优先级的区别
第一个区别是 unstructured task 会继承创建任务时的上下文环境。但是,detached task 不会。用 Swift 官方注释对 detached task 的描述就是:Run given throwing operation
as part of a new top-level task。也就是说,每一个 detached task 都是在并发模型的顶级作用域的。这也就意味着,detached task 不会继承来自创建环境的优先级设置。
为了观察到这个现象,我们先来了解下任务的优先级:
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
public struct TaskPriority : RawRepresentable {
public init(rawValue: UInt8)
public static let high: TaskPriority
public static let userInitiated: TaskPriority
public static let `default`: TaskPriority
public static let low: TaskPriority
public static let utility: TaskPriority
public static let background: TaskPriority
}
其中,我删掉了一些不必要的注释和代码,方便我们观察。在 TaskPriority
中一共有六个优先级。但真正起作用的只有四个,分别是:high / default / low / background
,它们在所有平台的 Swift 上均可使用。而 userInitialized / utility
则是 macOS 平台上的别名,它们分别对应 high / low
。
稍后,为了方便观察到任务的优先级,我们可以给 TaskPriority
添加一个扩展:
extension TaskPriority: CustomStringConvertible {
public var description: String {
switch self {
case .high, .userInitiated:
return ".high"
case .`default`:
return ".default"
case .low, .utility:
return ".low"
case .background:
return ".background"
default:
return ".unknown"
}
}
}
接下来的问题就是,如何观察到任务的优先级呢?我们可以使用 withUnsafeCurrentTask
函数,把之前 main
函数中创建各种任务的代码,改成这样:
Task(priority: .userInitiated) {
withUnsafeCurrentTask { task in
let priority = task?.priority.description ?? "unknown"
print("Start unstructured task at \(priority) priority.")
}
Task.detached {
withUnsafeCurrentTask { task in
let priority = task?.priority.description ?? "unknown"
print("Start playing BGM at \(priority) priority.")
}
}
let meal = Task { () -> Meal in
withUnsafeCurrentTask { task in
let priority = task?.priority.description ?? "unknown"
print("Start cooking at \(priority) priority.")
}
return try await makeDinnerWithThrowingTaskGroup()
}
try await eat(meal)
Task.detached {
print("Stop playing BGM.")
}
exit(EXIT_SUCCESS)
}
可以看到,withUnsafeCurrentTask
接受一个 closure 参数。这个 closure 自身则有一个 UnsafeCurrentTask?
类型的参数,表示当前任务。我们可以通过这个参数,访问当前任务的若干属性。例如在我们的例子中,就打印了当前任务的优先级。
要注意的是,就像
UnsafeCurrentTask
这个类型的名字一样,我们不能把这个参数拷贝到withUnsafeCurrentTask
的 closure 参数外使用,它只能在当前 closure 参数的 body 里使用,否则,会有未预期的结果。
接下来,执行一下,就会看到类似这样的结果:
Start unstructured task at .high priority.
Start cooking at .high priority.
Start playing BGM at .default priority.
Marinate meat
Chopping vegetables
Preheat oven.
Cook 300 seconds.
Yum!
Stop playing BGM.
这里,有两点值得我们注意:
- 首先,playing 和 cooking 的任务执行顺序,和我们定义的顺序并不相同,这刚好印证了之前我们说到的话题;
- 其次,cooking 任务继承了创建环境的
.high
优先级,而 playing 任务并没有,它仍旧是默认的.default
优先级,因为它没有继承创建任务时的上下文环境;
detached task 不能修改 actor 的状态
Detached task 和 unstructured task 的另外一个重要区别,是前者不能直接修改 actor
的状态,但后者可以。这是因为 unstructured task 会继承 actor
的上下文环境,并这个上下文环境中执行,因此这个任务不是并行的。但 detached task 则不然,它仍旧会在自己的上下文环境中执行,对于 actor
来说,这是一个并行的任务,actor
不允许它直接修改自己的状态。
这里我们先看一个最简单的例子感受下这个差别,关于 actor
我们后面在专门安排内容来解释它的用法。
actor AtomicIncrementor {
private var value: Int = 0
func current() -> Int {
return value
}
func increment() {
Task {
value += 1
}
}
}
这是一个线程安全的自增计数器。在 increment
方法里,我们使用一个 unstructured task 把 value
的值加 1。刚才我们说过,Task
的上下文环境和 actor
是一样的,因此 Task
在 actor
里并不是并行的,所以我们可以在 Task
里修改 value
。但如果我们用 detached task,就会发生错误了:
func increment() {
/// Cannot compile:
Task.detached {
self.value += 1
}
}
Swift 会提示我们:Actor-isolated property 'value' cannot be mutated from a Sendable closure。这是因为 detached task 有自己独立的上下文环境,这个环境相对于 actor
来说,是并行的,因此 actor
不允许它直接修改自己的状态。
What's next?
了解了结构化并发模型中的各种任务之后,下一节我们讨论另外一个问题:如何把基于回调函数的 API 迁移到结构化并发模型里。