Published on

Detached Tasks in Swift

Authors
  • Name
    Twitter

异步地运行给定的会抛出错误的操作,作为一个新的顶级任务。

它的api是如何定义的

@discardableRusult
static func detached(
    priority: TaskPriority? = nil,
    operation: sending @escaping @isolated(any) () async throws -> Success
) -> Task<Success, Failure>

初一看光是这个定义就会让人很头大,遇到这种问题,我们可以慢慢来进行拆解

  1. 它是一个Task的类方法,传入两个参数,返回结果是 Task<Success, Failure>,这个结果是一个泛型结构体。是Swift Concurrency 并发框架中的核心组件,它表示一个异步任务,可以在并发环境中运行,并通过 async/await 机制处理结果或错误。
  • Success: 表示任务成功完成时返回的结果类型,可以是任何类型(包括Void)
  • Failure: 表示任务失败时抛出的错误类型,必须符合 Error 协议 (通常是 any Error 或具体错误类型)。

Task<Success,Failure>主要用于以下场景:

  • 执行异步操作:启动一个异步任务,运行async函数或闭包。
  • 处理并发:支持结构化并发(如 withTaskGroup )和非结构化并发 (如 Task.detached)
  • 管理结果和错误: 通过 await task.value 获取成功结果,或者通过 try 捕获失败错误。

常见的类型组合:

  • Task<Void, Never> 表示无返回值且永不失败的任务,常用于无需结果的异步操作。
Task<Void, Never> {
    await someAsyncFunction()
}
  • Task<SomeType, any Error> 表示返回特定类型结果,可能抛出错误的异步任务。
Task<Int, any Error> {
    return try await fetchNumber()
}
  1. @discardableRusult 是一个属性(attribute),用于修饰函数的返回值,告诉编译器该函数的返回值可以被忽略,而不会触发”未使用返回值“的警告。
  2. 参数 priority 是一个可选参数,表示这个任务的优先级
  3. 参数 operation: sending @escaping @isolated(any) () async throws -> Success
  • Success: 泛型返回类型,表示闭包成功执行时返回的结果类型(如 String、Int 或自定义类型)。
  • () : 闭包没有参数(空参数列表)。
  • async: 闭包是异步的,必须在 async 上下文中通过 await 调用。
  • throws: 闭包可能抛出错误,需要通过 try 处理。
  • @escaping: 闭包是逃逸闭包,意味着它可能在函数返回后被调用,存储在外部作用域中。
  • @Sendable: 闭包符合 Sendable 协议,保证它可以安全地在并发上下文(如不同线程或 Actor)之间传递。
  • @isolated(any): 表示闭包可以在任意隔离域(Actor 或全局并发上下文)中运行,提供了灵活的隔离控制。 总结:是一个异步、可能抛出错误、返回 Success 的逃逸闭包,适合并发任务。

Discussion

If the operation throws an error, this method propagates that error. Don’t use a detached task if it’s possible to model the operation using structured concurrency features like child tasks. Child tasks inherit the parent task’s priority and task-local storage, and canceling a parent task automatically cancels all of its child tasks. You need to handle these considerations manually with a detached task. You need to keep a reference to the detached task if you want to cancel it by calling the Task.cancel() method. Discarding your reference to a detached task doesn’t implicitly cancel that task, it only makes it impossible for you to explicitly cancel the task.

如果 operation 抛出错误,Task.detached 方法会将该错误传播出去。

不要在可以使用结构化并发特性(如子任务)来建模操作时使用分离任务(detached task)。子任务会继承父任务的优先级和任务本地存储,并且取消父任务会自动取消其所有子任务。而对于分离任务,你需要手动处理这些事项。

如果你想通过调用 Task.cancel() 方法取消分离任务,必须保留对该任务的引用。丢弃对分离任务的引用并不会隐式取消该任务,只是让你无法显式取消它。

示例

import SwiftUI

struct ContentView: View {
    @Binding var text: String
    @State private var task: Task<String, any Error>?

    var body: some View {
        VStack {
            Text(text)
            Button("Run Detached Task") {
                task = Task.detached {
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                    guard !Task.isCancelled else { throw CancellationError() }
                    return "Task Completed!"
                }
                Task { // 主线程更新
                    do {
                        text = try await task!.value
                    } catch {
                        text = "Error: \(error)"
                    }
                }
            }
            Button("Cancel Task") {
                task?.cancel() // 手动取消
            }
        }
    }
}

说明:

  • task 保留对 Task.detached 的引用,允许通过 task.cancel() 取消。
  • operation 抛出 CancellationError 时,错误被传播并更新到 @Binding var text
  • 相比结构化并发,Task.detached 需要手动管理取消和优先级。