SC #8:取消 Task

发布: (2026年1月20日 GMT+8 04:09)
6 min read
原文: Dev.to

Source: Dev.to

注意: 在 Swift 中,取消 Task 并不保证执行会立即停止。每个 Task 必须手动检查是否已被取消(例如,使用 Task.checkCancellation()Task.isCancelled)。

1. 基本示例:下载图片

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        var imageRequest = URLRequest(url: imageURL)
        imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]

        print("Starting network request...")
        let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

        return UIImage(data: imageData)
    }

    // La tarea se cancela inmediatamente después de ser creada.
    imageTask.cancel()

    // Esperamos el valor (o el error) de la tarea.
    return try await imageTask.value
}

发生了什么

步骤描述
1️⃣将对 URLSession.shared.data(for:) 的调用包装在返回 UIImage?Task 中。
2️⃣使用 imageTask.cancel() 取消任务。
3️⃣使用 try await imageTask.value 等待结果。
4️⃣网络请求在取消生效之前就已启动,但 URLSession 会遵循取消信号并中止请求。

控制台输出

Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"

2. Task.checkCancellation()

Task.checkCancellation() 会在任务已被取消时自动抛出 CancellationError

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        // ...

        try Task.checkCancellation()               // 因为任务在调用 `checkCancellation()` 时已经被取消,网络请求不会执行。
    }
}

3. Task.isCancelled

Task.isCancelled 允许在不抛出异常的情况下检查取消状态;我们可以手动处理。

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://httpbin.org/image")!
        // ...

        guard Task.isCancelled == false else {
            print("Image request was cancelled")
            return nil
        }

        let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

        // ...
    }

    imageTask.cancel()
    return try await imageTask.value
}

输出

Image request was cancelled

4. 是否需要将 async 函数包装在 Task 中?

只有在我们想要取消操作时才需要显式创建 Task。如果不需要手动取消,可以直接编写不带包装器的函数:

func fetchImage() async throws -> UIImage? {
    let imageURL = URL(string: "https://httpbin.org/image")!
    var imageRequest = URLRequest(url: imageURL)
    imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]

    try Task.checkCancellation()

    let (imageData, _) = try await URLSession.shared.data(for: imageRequest)

    try Task.checkCancellation()

    return UIImage(data: imageData)
}

5. SwiftUI 中的手动取消

5.1 保存任务引用

@State private var image: UIImage?
@State private var imageDownloadingTask: Task?

var body: some View {
    VStack {
        if let image {
            Image(uiImage: image)
        } else {
            Text("Loading...")
        }
    }
    .onAppear {
        imageDownloadingTask = Task {
            do {
                image = try await fetchImage()
                print("Image loading completed")
            } catch {
                print("Image loading failed: \(error)")
            }
        }
    }
    .onDisappear {
        imageDownloadingTask?.cancel()
    }
}

5.2 使用 .task(priority:_: ) 修饰符

SwiftUI 在视图消失时会自动创建并取消任务。

@State private var image: UIImage?

var body: some View {
    VStack {
        if let image {
            Image(uiImage: image)
        } else {
            Text("Loading...")
        }
    }
    .task {
        do {
            image = try await fetchImage()
            print("Image loading completed")
        } catch {
            print("Image loading failed: \(error)")
        }
    }
}

重要提示: .task 中的代码会在任何由 .onAppear 启动的代码之前执行。然而,任务的确切执行时机并不保证;.onAppear 中的同步代码可能会在 .task 的任务之前执行。

6. 任务层级的取消(父任务 → 子任务)

当一个 父任务 被取消时,所有的 子任务 都会收到取消信号。每个子任务必须显式检查其状态(isCancelledcheckCancellation())。如果不检查,它们将继续执行。

示例

func parentTask() async {
    let handler = Task {
        print("Parent task started")
        async let child1 = childTask(id: 1)
        async let child2 = childTask(id: 2)

        // Esperamos a que ambas subtareas terminen.
        _ = try await [child1, child2]
    }

    // Simulamos algún trabajo y luego cancelamos.
    try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 s
    handler.cancel()

    do {
        _ = try await handler.value
    } catch let error as CancellationError {
        print("La tarea fue cancelada")
    } catch {
        print("Hubo otro error")
    }

    print("Parent task finished")
}

func childTask(id: Int) async throws -> Int {
    for i in 1..<4 {
        try Task.checkCancellation()                // <‑‑ verifica cancelación
        try await Task.sleep(nanoseconds: 100_000_000) // 0.1 s
        print("Subtarea: \(id), paso: \(i)")
    }
    print("Terminé la subtarea \(id)")
    return id
}

可能的输出

  • 如果父任务在子任务完成之前被取消,childTask 的每一次迭代都会抛出 CancellationError,并且执行会停止。
  • 如果某个子任务 检查取消状态,即使父任务已被取消,它仍会继续运行。

7. 快速概览

工具功能是否抛出异常?
task.cancel()标记任务应停止。
Task.checkCancellation()如果任务已被取消,则抛出 CancellationError
Task.isCancelled如果任务已被取消则返回 true
(在 guard/if 中使用)
.task {} (SwiftUI)创建一个任务,当视图消失时会自动取消。否(但内部任务可能会抛出)

有了这些概念,你就可以 控制优化 在 Swift 和 SwiftUI 应用中对异步操作的取消。祝编码愉快!

btarea: \(id)")
  return id
}
Back to Blog

相关文章

阅读更多 »

SC #4: async/await 语法

async/await 函数声明:异步函数必须使用关键字 async 标记。如果函数可能抛出错误,则添加关键字 catch……