iOS Swift 并发中的任务组织与取消

举报
shenlan9755 发表于 2026/09/08 09:24:45 2026/09/08
【摘要】 iOS Swift 并发中的任务组织与取消Swift 并发把异步流程写成了接近同步代码的形式,但任务仍然具有生命周期、优先级和取消状态。如果只关注 async 与 await 的语法,而没有设计任务归属,就容易出现页面退出后继续更新界面、搜索结果乱序、并发子任务失败关系不明确等问题。 一、让界面状态在主执行域更新页面模型通常需要在主执行域修改可观察状态。可以直接为整个模型声明 MainAc...

iOS Swift 并发中的任务组织与取消

Swift 并发把异步流程写成了接近同步代码的形式,但任务仍然具有生命周期、优先级和取消状态。如果只关注 asyncawait 的语法,而没有设计任务归属,就容易出现页面退出后继续更新界面、搜索结果乱序、并发子任务失败关系不明确等问题。

一、让界面状态在主执行域更新

页面模型通常需要在主执行域修改可观察状态。可以直接为整个模型声明 MainActor,减少每个方法重复切换线程的代码。

@MainActor
final class ArticleViewModel: ObservableObject {
    enum State {
        case idle
        case loading
        case content([Article])
        case empty
        case failed(String)
    }

    @Published private(set) var state: State = .idle
    private let repository: ArticleRepository

    init(repository: ArticleRepository) {
        self.repository = repository
    }

    func load() async {
        state = .loading
        do {
            let articles = try await repository.fetchArticles()
            state = articles.isEmpty ? .empty : .content(articles)
        } catch is CancellationError {
            return
        } catch {
            state = .failed("数据加载失败")
        }
    }
}

取消并不是业务失败,因此通常不应该展示错误提示。真正的错误也应先转换成稳定的界面文案,避免直接暴露底层错误信息。

二、把任务绑定到页面生命周期

SwiftUI 的 task 修饰器会在视图出现时启动任务,并在视图离开时发出取消信号。

struct ArticleListView: View {
    @StateObject var viewModel: ArticleViewModel

    var body: some View {
        content
            .task {
                await viewModel.load()
            }
    }
}

UIKit 中可以由控制器持有任务,并在合适的生命周期中取消:

final class ArticleViewController: UIViewController {
    private var loadTask: Task<Void, Never>?
    private let viewModel: ArticleViewModel

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        loadTask = Task {
            await viewModel.load()
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        loadTask?.cancel()
        loadTask = nil
    }
}

是否在 viewDidDisappear 取消,要根据页面业务决定。如果任务需要在短暂遮挡期间继续,就应选择更准确的停止时机。

三、理解取消是协作式的

调用 cancel 只会设置取消状态,不会强行终止所有代码。挂起操作可能检查取消,长时间同步计算则需要主动检查。

func transform(_ records: [RawRecord]) async throws -> [Record] {
    var output: [Record] = []
    output.reserveCapacity(records.count)

    for (index, record) in records.enumerated() {
        if index.isMultiple(of: 100) {
            try Task.checkCancellation()
        }
        output.append(convert(record))
    }
    return output
}

如果底层封装忽略取消,上层即使不再接收结果,网络或计算资源仍可能继续消耗。因此,数据层也需要把取消传递到底层能力。

四、用 async let 表达共同成败

当多个结果缺一不可时,async let 可以并发执行,并保持结构化的父子关系。

func loadHome() async throws -> HomeData {
    async let profile = repository.fetchProfile()
    async let orders = repository.fetchOrders()
    async let notices = repository.fetchNotices()

    return try await HomeData(
        profile: profile,
        orders: orders,
        notices: notices
    )
}

父任务离开作用域时,未完成的子任务会被取消。如果其中一个结果失败,整体加载失败。这段结构直接体现了“共同成败”的业务含义。

五、使用任务组处理动态数量任务

任务数量在运行时才能确定时,可以使用任务组:

func loadDetails(ids: [String]) async throws -> [Detail] {
    try await withThrowingTaskGroup(of: Detail.self) { group in
        for id in ids {
            group.addTask {
                try await repository.fetchDetail(id: id)
            }
        }

        var details: [Detail] = []
        for try await detail in group {
            details.append(detail)
        }
        return details.sorted { $0.id < $1.id }
    }
}

任务完成顺序不保证与添加顺序一致。如果界面或业务依赖原始顺序,需要携带索引并在收集后恢复,而不能假设返回顺序稳定。

六、解决搜索结果乱序

用户连续输入时,旧搜索可能比新搜索更晚返回。保存并取消上一任务,可以确保只接受最新输入。

@MainActor
final class SearchViewModel: ObservableObject {
    @Published private(set) var items: [SearchItem] = []
    private var searchTask: Task<Void, Never>?
    private let repository: SearchRepository

    func search(keyword: String) {
        searchTask?.cancel()
        searchTask = Task {
            do {
                try await Task.sleep(nanoseconds: 300_000_000)
                let result = try await repository.search(keyword: keyword)
                try Task.checkCancellation()
                items = result
            } catch is CancellationError {
                return
            } catch {
                items = []
            }
        }
    }
}

示例中的空数组仅代表产品明确约定的失败展示。如果业务要求保留旧结果,则不应在失败时清空。

七、谨慎使用非结构化任务

普通 Task 会继承当前执行上下文中的部分信息,但它仍需要有明确持有者。分离任务更适合真正独立、无需继承当前任务关系的工作,不适合为了绕过生命周期而随意使用。

创建任务前可以先回答三个问题:谁持有它,谁负责取消它,失败后由谁处理。任何一个问题没有答案,都意味着任务边界还不清晰。

八、测试并发时序

并发测试应覆盖成功之外的路径:

  • 页面消失后是否停止更新界面。
  • 连续搜索时是否只展示最后一次输入的结果。
  • 某个并发子任务失败时,其他任务是否按业务规则取消。
  • 取消是否被误显示为错误。
  • 动态任务返回顺序变化时,最终排序是否正确。

使用可控制的仓库替身和延迟,可以稳定制造先后顺序,不必让测试依赖真实网络环境。

总结

Swift 并发的重点是任务结构,而不仅是语法。界面状态放在主执行域,任务与页面生命周期绑定,耗时工作主动响应取消,再根据共同成败或独立失败的业务关系选择并发方式,才能避免异步结果错乱并保持代码可推理。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。