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 是一样的,因此 Taskactor 里并不是并行的,所以我们可以在 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 迁移到结构化并发模型里。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们