首页 / Swift 编程语言教程 / 并发常见陷阱与最佳实践

Swift 编程语言教程

并发常见陷阱与最佳实践

本教程共 93 篇 · 第 87 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程并发最佳实践数据竞争死锁Sendable陷阱

本节目标:识别 Swift 并发里最易踩的坑,并掌握对应的写法习惯,让并发代码既安全又流畅。

前面几章把并发的语法一件件拆开了讲。这一章做收口:把初学者最容易栽进去的坑集中亮出来,再配上经过验证的最佳实践。把这一章记住,你写并发代码时能少掉一半 bug。

1-1 陷阱一:忘了写 await,或写错地方

await 不是装饰,它是挂起点的标记。最常见的编译错误就是”这里调用了异步函数却没写 await”。更隐蔽的是 tryawait 的遗漏:既抛错又异步的函数,两个都得写。

// 错误:漏了 try 和 await
let data = listPhotos(inGallery: "x")

// 正确
let data = try await listPhotos(inGallery: "x")

实践:看到异步函数调用就本能确认 await(和必要的 try)在场。编译器报错”call is ‘async’""call can throw”时,第一时间补这两个词。

1-2 陷阱二:同步代码里直接调异步

这是迁移老代码时最痛的一点:你没法在同步函数里写 await,也没法”等”一个异步结果回来。硬要写会被编译拒绝。

func bad() -> String {
    // return await fetchName()  // 编译错误:同步上下文不能 await
}

实践:自顶向下改。先让最外层函数变成 async(或放进 Task {}),再逐层把被它调用的函数也改成异步。不要试图在底层”包一层”把异步变同步——Swift 故意不支持,那会引入死锁和竞争。

1-3 陷阱三:长任务不检查取消

你以为 task.cancel() 一调用任务就没了,结果它一路跑完。原因在第 82 章讲过:取消是协作式的,任务体里必须有取消检查才会停。

// 危险:从不检查取消,cancel() 等于没用
func work() async {
    for i in 0..<1_000_000 {
        doExpensiveStep(i)   // 永远跑到头
    }
}

// 安全:周期性检查
func workSafe() async throws {
    for i in 0..<1_000_000 {
        try Task.checkCancellation()
        doExpensiveStep(i)
    }
}

实践:循环、批量处理、网络等待等长任务里,埋入 try Task.checkCancellation()Task.isCancelled 检查。需要清理用 isCancelled,直接放弃用 checkCancellation()

1-4 陷阱四:跨并发域传了不可发送的数据

把含可变状态的普通类实例,从一个任务传给另一个任务或 actor,编译期就会报”类型不 Sendable”。这是好事——它在拦数据竞争。

class MutableBox {       // 不可发送:有可变状态又无保护
    var value = 0
}

actor Logger {
    func log(_ box: MutableBox) { }   // 编译错误:MutableBox 不是 Sendable
}

实践:要跨域传递的数据,优先用纯值类型(结构体/枚举),自动可发送;必须用类时,做成全只读(let 属性)或自带隔离(如标 @MainActor)。别为了通过编译就谎标 : Sendable

1-5 陷阱五:在后台线程更新了 UI

界面更新没跑在主 actor 上,后果是偶发卡顿、偶发崩溃,报错指向 UI API。根因是你在一个没标 @MainActor 的异步函数里直接改了界面状态。

func load() async {
    let img = await download()   // 后台
    imageView.image = img        // 危险:没在主 actor 上
}

// 正确:切回主 actor 再更新
func loadSafe() async {
    let img = await download()
    await MainActor.run { imageView.image = img }
}

实践:所有读写界面状态的代码,确保位于 @MainActor 标记的上下文。耗时计算留在后台,最后一步 await 切回主 actor 更新。也可以用 MainActor.run { } 临时切回。

Warning

别把长耗时逻辑也圈进 @MainActor,那等于把并发全串行回主线程,界面照样卡。精准区分”耗时”和”更新”:前者后台,后者主 actor。

1-6 陷阱六:用 actor 却忘了 await

访问 actor 的属性或方法必须写 await,因为它要排队跨边界。漏写直接编译错。还有人误以为 actor 内部也到处要 await——其实 actor 自己的同步方法里直接访问,不写 await

实践:跨 actor 边界(从外部访问)一律 await;actor 内部的同步逻辑自然数直连,保持无挂起点以杜绝中途被打断。

1-7 最佳实践清单

把下面几条当习惯刻进肌肉记忆:

一、优先用值类型。结构体、枚举天然值语义,易推理、可发送,配合 let 更稳。

二、优先用结构化并发(async let、任务组),少用非结构化 Task {},尽量不用 Task.detached

三、共享可变状态就封装进 actor,别自己用锁。actor 隔离在编译期强制,比锁清晰。

四、长任务必埋取消检查,让取消真正生效。

五、UI 全在主 actor,耗时全在后台,最后一步切回更新。

六、跨域数据用 Sendable,宁可用值类型也不要谎标协议。

七、用 for await 处理流式数据,别硬攒大数组。

Tip

一个快速自检法:写完整段并发代码,问自己三个问题——“哪些会并行?“(看 async let/任务组)、“会不会被取消?“(看取消检查)、“共享数据谁保护?“(看 actor/Sendable)。三个都答得上来,基本就稳了。

1-8 关于版本的提醒

本章所有语法(async/await、actor、Sendable、@MainActor、AsyncSequence、结构化并发)在 Swift 5.5 起陆续稳定,到 6.3.2 已完全可用,按稳定写法讲解即可。你可能会在网上看到一些更新的并发特性(如某些 6.4 beta 才出现的细化能力),它们需要显式标注「(6.4 预览)」才可使用。本教程基线为 6.3.2,未标注预览的特性都应视为已稳定,放心使用。

1-9 一个完整的小例子串起来

把前面所有工具拼到一个小场景里:并发下载多张图、更新进度、可被取消、UI 在主 actor。伪代码长这样:

@MainActor
func updateProgress(_ value: Double) {
    progressBar.progress = Float(value)   // 主 actor 上更新界面
}

func downloadAll(_ urls: [URL]) async {
    await withThrowingTaskGroup(of: Void.self) { group in
        for (i, url) in urls.enumerated() {
            group.addTask {
                let data = try await fetch(url)       // 后台下载
                try Task.checkCancellation()          // 允许取消
                await updateProgress(Double(i) / Double(urls.count))
                save(data)
            }
        }
    }
}

这里 withThrowingTaskGroup 并行下载;Task.checkCancellation() 让取消生效;updateProgress@MainActor 保证界面更新在主线程;fetch 是普通异步函数。一个片段就把”并行、取消、隔离、UI 安全”全展示了——这正是现代 Swift 并发的惯用形态。

Tip

读到这如果你觉得”概念好多”,别慌。真实项目里它们是一套组合拳:结构化并发管并行、actor/Sendable 管共享、主 actor 管 UI、取消管退出。先照着模板写,写着写着就顺了。

1-10 调试并发问题的几个抓手

最后给点调试方向的建议,真出问题时不至于懵。

第一,善用 deinit 打印。怀疑对象没释放(循环引用),就在 deinit 里打日志,看该释放时释放了没。

第二,关注编译器关于 Sendableactor 隔离的报错,别急着用 unsafe 或强转绕开——那往往是设计有问题的信号,不是编译器的刁难。

第三,怀疑 UI 在非主线程更新时,把相关代码临时包进 MainActor.run 或用断点确认调用栈是否在主线程。

第四,取消不生效,先检查长任务里有没有 Task.checkCancellation()Task.isCancelled。绝大多数”取消没反应”都是漏了检查点。

Warning

并发 bug 往往”偶尔出现、难以复现”。不要因为某次没崩就以为没问题,编译器的并发检查(Sendable、隔离)才是你能依赖的硬保障。把代码写得让编译器满意,比靠测试碰运气靠谱。

1-11 小结:模块十收官

并发的本质,是让程序在等待时不被卡死,同时避免多段代码抢同一份数据。Swift 用一套语言级方案把安全前置到编译期:async/await 写清晰异步代码、结构化并发管生命周期、actor 与 Sendable 防数据竞争、主 actor 守界面、AsyncSequence 流式消费。

避开本章列的六个坑、守住七条实践,你就能写出既快又稳的并发代码。到这一章,模块十”并发编程”全部讲完,Swift 语言核心(语法、类型、面向对象、内存、并发)也已覆盖完整。