实战指南 | Swift 并发中的任务取消机制

开发 前端
这篇文章会讲清楚任务取消的原理、如何正确使用它,以及如何写出高效又优雅的代码。​

前言 

Swift 并发提供了一种协作式取消(cooperative cancellation) 机制,来让任务在需要时自己退出。简单来说,Swift 不会强行终止你的任务,但它会告诉你任务已经被标记为取消,至于你要不要停下来,那是你自己的决定。

这篇文章会讲清楚任务取消的原理、如何正确使用它,以及如何写出高效又优雅的代码。

什么是协作式取消? 

协作式取消的核心思想是:

  • 调用方(比如 SwiftUI)没法直接终止任务,只能“标记”为取消。
  • 任务本身需要定期检查这个标记,并决定要不要提前终止。
  • 你可以选择直接返回、提供部分结果,或者继续执行,全看你的业务逻辑。

简单来说,Swift 只是给你一个“信号”——“嘿,这个任务已经没用了,看看你要不要停下来”。

如何用 Task API 处理任务取消 

来看个例子,这是一个 SwiftUI 界面,用户输入搜索内容时,会触发异步搜索。

struct ContentView: View {
    @Stateprivatevar store = Store()
    @Stateprivatevar query = ""
    
    var body: some View {
        NavigationStack {
            List(store.results, id: \.self) { result in
                Text(verbatim: result)
            }
            .searchable(text: $query)
            .task(id: query) {
                await store.search(matching: query)
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

这里最关键的是 task(id: query) 这行代码:

  • 当 query 变化时,SwiftUI 会启动一个新的搜索任务。
  • 同时,它会标记上一个任务为“已取消”,但不会立刻终止它。
  • 如果旧任务里没有检查取消状态,它可能仍然会跑完所有逻辑。

这意味着,如果用户输入了很多字符,可能会同时存在多个搜索任务,这就是为什么我们要手动处理取消逻辑。

在异步方法中正确处理取消 

假设 Store 负责查询数据,我们的 search(matching:) 方法如下:

import HealthKit

@MainActor @Observablefinalclass Store {
    private(set) var results: [HKCorrelation] = []
    privatelet store = HKHealthStore()
    
    func search(matching query: String) async {
        let foodQuery = HKSampleQueryDescriptor(
            predicates: [.correlation(type: .init(.food))],
            sortDescriptors: []
        )
        
        do {
            let food = try await foodQuery.result(for: store)
            
            tryTask.checkCancellation()  // 检查任务是否已取消
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

这里有个关键点:**Task.checkCancellation()**。

  • 这个方法会抛出一个错误,如果任务已经被取消,它就会立刻停止执行,后续的代码不会再运行。
  • 这样可以避免执行一些不必要的逻辑,比如过滤数据、更新 UI 等。
  • 如果任务被取消,我们直接把 results 置空,这样用户不会看到过时的搜索结果。

在多个步骤中检查取消状态 

如果你的异步代码有多个步骤,比如先获取数据、然后再做一些处理,那你可能需要在多个关键点检查任务是否已取消,否则即使任务已经无效了,它可能还会跑完整个流程。

import HealthKit

@MainActor @Observablefinalclass Store {
    private(set) var results: [HKCorrelation] = []
    privatelet store = HKHealthStore()
    
    func search(matching query: String) async {
        let foodQuery = HKSampleQueryDescriptor(
            predicates: [.correlation(type: .init(.food))],
            sortDescriptors: []
        )
        
        do {
            let food = try await foodQuery.result(for: store)
            
            tryTask.checkCancellation()  // 第一次取消检查
            
            // 假设这里有额外的数据处理
            tryTask.checkCancellation()  // 第二次取消检查
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

为什么要多次检查?

  • 如果 foodQuery 运行了一段时间,任务被取消了,我们希望尽早停下来,而不是等所有代码都跑完。
  • 某些任务可能是分步执行的,比如先获取原始数据,再处理数据。如果第一步完成了,但任务已经取消了,我们就没必要继续处理数据。

用 isCancelled 进行检查 

除了 Task.checkCancellation() 之外,Swift 还提供了 Task.isCancelled 这个属性,它是一个布尔值,你可以用它更灵活地处理任务取消:

actor SearchService {
    privatevar cachedResults: [HKCorrelation] = []
    privatelet store = HKHealthStore()
    
    func search(matching query: String) async throws -> [HKCorrelation] {
        guard !Task.isCancelled else {
            return cachedResults  // 任务取消了,直接返回缓存
        }
        
        let foodQuery = HKSampleQueryDescriptor(
            predicates: [.correlation(type: .init(.food))],
            sortDescriptors: []
        )
        
        let food = try await foodQuery.result(for: store)
        
        guard !Task.isCancelled else {
            return cachedResults  // 任务取消了,避免不必要的计算
        }
        
        cachedResults = food.filter { food in
            let title = food.metadata?["title"] as? String ?? ""
            return title.localizedStandardContains(query)
        }
        
        return cachedResults
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

两种方式的区别:

  • Task.checkCancellation():如果任务已取消,直接抛出错误,代码不再继续执行。
  • Task.isCancelled:任务是否继续执行,由你自己决定,比如可以提前返回缓存数据,而不是直接终止

手动取消任务 

通常情况下,Swift 会帮你管理任务的取消,但如果你想手动创建和取消任务,也可以用 Task:

struct ExampleView: View {
    @Stateprivatevar store = Store()
    @Stateprivatevar task: Task<Void, Never>?
    
    var body: some View {
        VStack {
            Button("开始任务") {
                task = Task {
                    await store.fetch()
                }
            }
            
            Button("取消任务") {
                task?.cancel()
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

这里 task?.cancel() 只会标记任务为取消,但不会真的终止它,所以你仍然需要在 fetch() 里检查 Task.isCancelled 或 Task.checkCancellation()。

总结 

  • Swift 不会自动终止任务,只会标记它为取消。
  • 用 Task.checkCancellation() 可以立即终止任务,防止执行不必要的逻辑。
  • 用 Task.isCancelled 可以更灵活地决定如何处理取消。
  • 如果任务有多个异步步骤,应该在关键点多次检查取消状态。
  • 手动创建的任务可以用 .cancel() 取消,但仍然需要手动检查取消状态。

学会这些,你的 Swift 并发代码就能更高效、更优雅地处理任务取消,让用户体验更流畅!

责任编辑:姜华 来源: Swift社区
相关推荐

2014-07-29 11:20:28

Swift豆瓣电台编程实战

2022-04-26 08:41:38

Swift并发系统iOS

2025-03-19 09:02:18

Debouncing任务让步Swift

2024-04-09 08:04:42

C#结构await

2023-04-26 11:59:06

Swift异步编程

2024-05-11 08:31:20

中断机制插队机制React

2021-02-02 14:55:48

React前端高优先

2018-03-15 16:45:47

前端JavaScriptthis

2023-11-06 14:13:51

asyncio开发

2011-12-12 11:16:02

iOS并发编程

2024-08-09 10:59:01

KubernetesSidecar模式

2017-01-13 22:42:15

iosswift

2021-07-22 09:43:09

Golang语言并发机制

2020-11-20 07:51:02

JavaSPI机制

2009-06-02 10:32:30

Oracle并发处理

2013-12-12 16:44:25

Lua协程

2016-10-09 14:41:40

Swift开发ARC

2015-01-21 16:25:29

Swift指针

2015-07-08 16:43:02

Configurati

2015-11-23 10:07:19

Swift模式匹配
点赞
收藏

51CTO技术栈公众号