admin管理员组

文章数量:1035702

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

前言

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

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

什么是协作式取消?

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

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

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

如何用 Task API 处理任务取消

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

代码语言:swift复制
struct ContentView: View {
    @State private var store = Store()
    @State private var 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)
            }
        }
    }
}

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

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

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

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

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

代码语言:swift复制
import HealthKit

@MainActor @Observable final class Store {
    private(set) var results: [HKCorrelation] = []
    private let 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)
            
            try Task.checkCancellation()  // 检查任务是否已取消
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}

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

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

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

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

代码语言:swift复制
import HealthKit

@MainActor @Observable final class Store {
    private(set) var results: [HKCorrelation] = []
    private let 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)
            
            try Task.checkCancellation()  // 第一次取消检查
            
            // 假设这里有额外的数据处理
            try Task.checkCancellation()  // 第二次取消检查
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}

为什么要多次检查?

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

用 isCancelled 进行检查

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

代码语言:swift复制
actor SearchService {
    private var cachedResults: [HKCorrelation] = []
    private let 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
    }
}

两种方式的区别

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

手动取消任务

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

代码语言:swift复制
struct ExampleView: View {
    @State private var store = Store()
    @State private var task: Task<Void, Never>?
    
    var body: some View {
        VStack {
            Button("开始任务") {
                task = Task {
                    await store.fetch()
                }
            }
            
            Button("取消任务") {
                task?.cancel()
            }
        }
    }
}

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

总结

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

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

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

前言

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

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

什么是协作式取消?

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

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

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

如何用 Task API 处理任务取消

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

代码语言:swift复制
struct ContentView: View {
    @State private var store = Store()
    @State private var 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)
            }
        }
    }
}

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

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

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

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

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

代码语言:swift复制
import HealthKit

@MainActor @Observable final class Store {
    private(set) var results: [HKCorrelation] = []
    private let 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)
            
            try Task.checkCancellation()  // 检查任务是否已取消
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}

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

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

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

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

代码语言:swift复制
import HealthKit

@MainActor @Observable final class Store {
    private(set) var results: [HKCorrelation] = []
    private let 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)
            
            try Task.checkCancellation()  // 第一次取消检查
            
            // 假设这里有额外的数据处理
            try Task.checkCancellation()  // 第二次取消检查
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}

为什么要多次检查?

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

用 isCancelled 进行检查

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

代码语言:swift复制
actor SearchService {
    private var cachedResults: [HKCorrelation] = []
    private let 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
    }
}

两种方式的区别

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

手动取消任务

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

代码语言:swift复制
struct ExampleView: View {
    @State private var store = Store()
    @State private var task: Task<Void, Never>?
    
    var body: some View {
        VStack {
            Button("开始任务") {
                task = Task {
                    await store.fetch()
                }
            }
            
            Button("取消任务") {
                task?.cancel()
            }
        }
    }
}

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

总结

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

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

本文标签: 「实战指南 」Swift 并发中的任务取消机制