从警告到编译错误:一次 Swift 6 严格并发迁移实录
引言:升级之后,项目里全是错误
打开 Swift 6 语言模式的那一刻,很多人的第一反应是:这项目还能不能要了?
几百条 Sendable 相关的红字错误,散落在网络层、缓存层、通知回调里。最省事的做法是到处加 @unchecked Sendable,让编译器闭嘴。但这相当于把编译器能替你发现的问题,留到了线上崩溃时才暴露。
这篇文章记录我在实际项目里迁移时踩到的四个坑,以及一份可以直接照着走的清单。
一、先搞清 Swift 6 在检查什么
严格并发检查只关心一件事:非 Sendable 的值,有没有跨越隔离域被共享。
Sendable表示"这个值可以安全地跨并发域传递"。值类型(struct/enum)在成员都可 Sendable 时自动满足;引用类型默认不满足。- 隔离域由 actor 或全局 actor(最典型就是
@MainActor)划定。跨域访问必须await。 - 全局变量必须是
let+ Sendable,否则直接报错。
理解这两句话,大部分报错都能解释清楚。
二、坑一:actor 是可重入的,它不负责原子性
很多人把 actor 当成"自动加锁的对象",于是写出这样的缓存:
actor ImageCache { private var storage: [URL: Data] = [:] func data(for url: URL) async throws -> Data { if let cached = storage[url] { return cached } let (data, _) = try await URLSession.shared.data(from: url) storage[url] = data return data }}
这段代码没有数据竞争,编译器不会再报错。但它有一个逻辑上的竞态:两个协程同时请求同一张图时,都会先检查缓存,都没有命中,然后下载两遍。
原因在于 actor 的语义:它保证同一时刻只有一段同步代码在执行,却不保证一次方法调用整体是原子的。遇到 await 时,执行权就会交还给调用方,其他任务可以进来插队。
修复方式是让同一 URL 的请求在内部去重:
actor ImageCache { private var storage: [URL: Data] = [:] private var inFlight: [URL: Task<Data, Error>] = [:] func data(for url: URL) async throws -> Data { if let cached = storage[url] { return cached } if let task = inFlight[url] { return try await task.value } let task = Task { () throws -> Data in try await URLSession.shared.data(from: url).0 } inFlight[url] = task do { let data = try await task.value storage[url] = data inFlight[url] = nil return data } catch { inFlight[url] = nil throw error } }}
结论:actor 保证的是互斥访问,不保证业务上的原子性。凡是在 await 前后依赖状态不变的地方,都要自己再想一遍。
三、坑二:把 @MainActor 当成常用的隔离域
一旦遇到隔离相关的报错,最省事的做法就是给所有类加上 @MainActor。这确实能让项目编译通过,但代价是网络、解析、磁盘 IO 全部挤到主线程。
正确的思路是按数据归属划分隔离域,而不是按"能不能编译"来划分:
// 只在 UI 状态需要共享的地方用 MainActor@MainActorfinal class FeedViewModel { private(set) var items: [Item] = [] private let service = FeedService() // actor,处理网络与解析 func refresh() async throws { items = try await service.loadFeed() }} actor FeedService { func loadFeed() async throws -> [Item] { let (data, _) = try await URLSession.shared.data(from: feedURL) return try JSONDecoder().decode([Item].self, from: data) // 解析不占用主线程 }}
边界要清楚:UI 状态在主 actor 上,数据加工在 actor 里,两者之间用 await 传递 Sendable 的数组。
另外要注意:如果某个 nonisolated async 函数内部有较重的 CPU 计算,在 Swift 6.2 起可以用 @concurrent 标记它,让它真正跑在全局并发执行器上,而不是调用方所在的 actor 上。
四、坑三:Task.detached 不会继承调用方的隔离域
进入后台线程时,很多人第一反应是 Task.detached,然后就会撞上这类报错:
@MainActorfinal class FeedViewModel { private(set) var items: [Item] = [] func refresh() { Task.detached { let fetched = await self.fetch() self.items = fetched // 错误:Main actor-isolated property 不能在 nonisolated 上下文修改 } }}
Task.detached 不会继承任何调用方的上下文:不继承 actor 隔离、不继承优先级、不继承 task-local 值。它适合那些完全独立、与该调用方没有任何关系的任务;不应当把它当作一种通用的后台写法。
大部分场景用 Task {} 就够了:
func refresh() { Task { // 继承 @MainActor 隔离,写回 items 不需要任何额外操作 let fetched = await fetch() items = fetched }}
如果确实想让耗时逻辑离开主 actor,正确的做法是把它交给另一个隔离域(比如前面提到的 actor),再通过 await 把结果带回来,而不是用一个 detached 任务硬把隔离打断。
五、坑四:把 @unchecked Sendable 当成例外手段
编译器的报错有时确实让人心烦,但只要加上 @unchecked,错误就消失了。但 @unchecked 的意思是"我手动保证线程安全",编译器从此不再检查。如果代码里根本没有加锁,那它就只是个谎言。
如果确实需要用引用类型跨域共享,就要把约定写清楚,并且真正落实:
final class Counter: @unchecked Sendable { private let lock = NSLock() private var _value = 0 var value: Int { lock.withLock { _value } } func increment() { lock.withLock { _value += 1 } }}
规则只有一条:所有可变状态必须被同一把锁保护,且内部引用不得逃逸出去。满足不了这两点,就别用 @unchecked,改成 actor 或者值类型。
全局变量是同一类问题的另一种表现:
let formatter = DateFormatter() // 错误:全局作用域中非 Sendable 的 let@MainActor let formatter = DateFormatter() // 方案一:交由主 actor 隔离nonisolated(unsafe) let formatter = DateFormatter() // 方案二:自己保证只读/线程安全 // 更推荐:换成天然 Sendable 的值类型let style = Date.FormatStyle(date: .numeric, time: .shortened)
六、迁移清单
- 先别急着切到 Swift 6。在 Swift 5 模式下把 Strict Concurrency Checking 调成 Complete(
-strict-concurrency=complete),先把警告清零。 - 从边界推进:网络回调、通知、数据库、缓存,这些是数据跨域最频繁的地方,也是问题最集中的地方。
- 优先消除共享可变状态,让值类型和 actor 承担状态管理;加锁只是最后一步。
- 每次使用
@unchecked Sendable或nonisolated(unsafe)都要写明理由,并在 PR 里重点 review。 - 用 Thread Sanitizer 加压力测试保底:编译器能证明的东西有限,并发 bug 最终还是要在运行时验证。
结语
Swift 6 的严格并发并不是故意让开发者为难。它只是把过去要靠线上崩溃才能发现的问题,提前到了编译期。迁移过程中真正花时间的地方,在于重新想清楚哪些状态该由谁持有、在哪个隔离域里被修改,改语法本身反倒花不了多少时间。想明白这件事,报错自然就消失了。
- 点赞
- 收藏
- 关注作者
评论(0)