Swift 6.2 并发实战:默认 MainActor,有目的地逃逸
Swift 6.0 把数据竞争变成了编译错误,而在长达一年的时间里,让所有人为此付出了代价。严格并发检查器把寻常的 UI 代码变成了一面由 Sendable 违规和”main actor-isolated property can not be referenced from a nonisolated context”错误堆成的墙。诊断本身是对的(那些代码确实可能竞争),但海量的报错淹没了真正有价值的信号,于是许多团队要么停留在 Swift 5 模式,要么到处撒 @MainActor,直到报错安静下来。
Swift 6.2 改变的是默认行为,而不是规则本身。数据竞争安全的保证依然如故;变的是编译器的起点。采用新的默认设置,那面墙的大部分就会消失,因为编译器现在假设的,正是你的应用本就成立的事实:你的大部分代码运行在 main actor 上,而你是有目的地、在具名的地方离开它的。这正是我在 941 系列应用中所采用的模型1,它也是一段更宏大的演进脉络中的一环——2026 年 Swift 新特性综述沿着这条脉络,追溯了随后到来的 6.3 与 6.4 提案。下面讲讲它是如何运作的,以及一旦你启用它后,仍会咬你一口的六个错误。
太长不看
- SE-0466 让你可以把整个模块默认设为
@MainActor。 设置SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor,从此不必再为每个视图、模型和视图模型逐一加注解2。 - SE-0461 让 nonisolated 的
async函数默认在调用方的 actor 上运行(nonisolated(nonsending)),于是调用 async 代码不再强制触发一次 actor 跳转,也不再带来随之而来的跨边界Sendable错误3。 @concurrent是逃逸的出口。 给函数标注@concurrent,把繁重的 CPU 工作(解码、图像处理)有意且显式地推到后台线程上4。- Xcode 26 为新项目默认开启这两项。总开关是
SWIFT_APPROACHABLE_CONCURRENCY = YES5。 - 这个模型反转了负担:你不再需要证明每一行代码都可以安全地脱离主线程运行,而是把一切都留在主线程上,只去证明你离开它的那几处。
- 切换之后仍会留下六个具体的错误。一旦你看清其中的模式,这六个都有一行就能搞定的修法。
这场反转,以及它为何重要
旧模型(Swift 6.0):代码在你隔离它之前都是 nonisolated 的。每个触及 UI 状态的类型都需要 @MainActor,每个跨隔离边界的 async 调用都需要 Sendable 一致性,编译器会逐个标记出每一处缺口。对于一个 95% 的代码本就运行在主线程上的应用来说,你把时间花在了为这 95% 加注解,去描述一个从无疑问的事实。
新模型(Swift 6.2):代码在你离开它之前都在 main actor 上。SE-0466 让你可以把 main-actor 隔离声明为模块默认,于是一个视图、它的模型和它的辅助代码,无需任何一个注解就全都是 @MainActor2。SE-0461 随后取消了第二项税负:一个 nonisolated 的 async 函数现在会在调用它的那个 actor 上运行,而不是跳到全局执行器上,因此 await 它不会再把你拖过一条隔离边界,也不会要求作用域内的一切都满足 Sendable3。
这个心智模型,与 UI 应用实际的行为方式相吻合。默认在主线程上并不是一种妥协;对于一个其状态就是其视图的应用而言,这是事实。并发于是成了你伸手去取的例外——具名、受控,而不再是你要在每一行上严防死守的弥漫性状态。编译器的工作从”证明这段代码可以安全地并发运行”翻转为”你说了这段代码会并发运行,那就证明它是安全的”,而第二个问题被提及的地方要少得多。
Swift 核心团队也是这么说的。在 WWDC 2026 Swift Group Lab 上,Holly 描述说,如果以今天的眼光来设计这套并发模型,nonisolated 的 async 函数从第一天起就会在调用方的上下文中运行;她转述说,自己曾长期坚持全局并发池模型,后来是被真实项目中的种种问题说服而改变了想法。Doug 补充说,旧的默认设置把太多类型推向了 Sendable。(两段引述均转述自一份本地转录的实验室录音;Apple 没有为该场次发布字幕。)9
启用它
两个构建设置,在 Xcode 26 中都已是新项目的默认值,而在已有项目上也都值得显式设置5:
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES
第一项是 SE-0466:模块默认采用 main-actor 隔离。第二项是总开关,用于启用 approachable-concurrency 这一整套特性,其中就包括 SE-0461 的”调用方运行”行为。在 Swift 包中,你通过 swiftSettings 配合相应的 upcoming-feature 标志来设置同样的默认值,而不是使用 Xcode 的构建设置6。
在一个已有项目上同时开启这两项,错误数会大幅下降,因为检查器原先标记的那些大多是它此前无法假定为主线程的主线程代码。剩下的是一份简短的、真正属于边界情况的清单。它们值得逐个记住名字,因为每一处都是你的代码确实离开 main actor 的地方,而修法就是把这件事精确地说出来。
切换后仍然存在的六个错误
以下是在 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 下仍会出现的严格并发错误,取自将 941 系列应用迁移到该模型的过程1。每一个都是真实的边界,而非误报,这也正是为什么修法是一处精确的注解,而不是一次压制。
1. Sendable 类型上的纯函数。 一个 Sendable 枚举上的纯方法(一个 URL 构造器、一个格式化器)在模块默认下会继承 main-actor 隔离,随后在从 nonisolated 上下文调用时报错:“Call to main actor-isolated instance method in a synchronous nonisolated context.” 该方法不触及任何状态,所以把它隔离到 main actor 上是错的。给它标注 nonisolated7:
nonisolated func searchURL(for query: String) -> URL? { ... }
2. 默认参数中的单例静态属性。 static let shared = Foo() 在默认下是 main-actor-isolated 的,但默认参数的值是在调用方的上下文中求值的,而那往往是 nonisolated 的:“Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” 把这个静态属性设为 nonisolated。如果该类型是 Sendable(或因为你自己守护其状态而标为 @unchecked Sendable),你就不需要任何 unsafe 限定符:
final class KeychainProxySecretStore: @unchecked Sendable {
nonisolated static let shared = KeychainProxySecretStore()
}
3. 不可变的基础类型常量。 同样的默认参数问题也会击中一个普通常量:一个从 nonisolated 默认实参中被引用的 static let defaultInterval。修法完全相同,而该常量本就可以安全地共享:
nonisolated static let defaultInterval: TimeInterval = 15 * 60
4. 读取被捕获的 self 的 Task 体。 一个外层闭包捕获了 [weak self];在其内部,一个 Task { @MainActor in self?.foo() } 读取了那个被捕获的可选值:“Reference to captured var ‘self’ in concurrently-executing code.” 这个 Task 并发地从外围作用域读取了一个 var 绑定,这正是竞争所在。在 Task 边界处重新捕获 self,让该 Task 拥有一个不可变的绑定:
NotificationCenter.default.addObserver(...) { [weak self] _ in
Task { @MainActor [weak self] in
self?.value = next
}
}
5. 读取 main-actor 状态的 KVO 回调。 一个 webView.observe(\.canGoBack) { wv, _ in ... } 回调是 @Sendable 的,因此是 nonisolated 的,但 WKWebView.canGoBack 是 main-actor-isolated 的:“Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” KVO 是在修改该值的那个线程上同步投递的,而 WKWebView 的导航状态只在主线程上发生变化,所以这次读取是可靠的。用 MainActor.assumeIsolated 来断言它,这会彻底去掉那次 Task 跳转并保持同步8:
let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
MainActor.assumeIsolated {
guard let self else { return }
// safe to read webView?.canGoBack synchronously
}
}
assumeIsolated 是对编译器的一个承诺,而不是一个提问。只在运行时不变量确实成立的地方(一个有文档记载的主线程回调)使用它,因为一个错误的承诺带来的是崩溃,而不是警告。
6. 不该留在主线程上的繁重工作。 这一个检查器不会标记,而它恰恰是最需要你自己抓住的。在 main-actor 默认下,一个同步的 CPU 密集型方法(解码一个大的 JSON 负载、缩放一张图片)会运行在 main actor 上,卡顿你的 UI。默认会把你留在主线程上;@concurrent 则是你有意离开的方式4:
@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
try JSONDecoder().decode(Report.self, from: data)
}
@concurrent 把函数卸载到全局执行器上,并且按设计与 @MainActor、自定义全局 actor 以及 nonisolated(nonsending) 互斥:一个函数要么在它的调用方所在之处运行,要么有意地从那里跑开,绝不含糊4。新模型所要求的纪律,完全就活在这个模式里。一切触及 UI 的东西都留在主线程上,只有那些经测量确实需要后台线程的工作,才伸手去取 @concurrent。
当这些默认值不适合你时
默认在主线程上,适合应用:SwiftUI 和 UIKit 代码绝大多数都在主线程,这个默认与现实相符。它在两种情况下不那么合适,装作不是这样只会浪费你的时间。
- 没有 UI 的库或框架目标。 一个网络层、一个解析器或一个数据引擎没有理由默认采用 main actor,这样做会逼着你几乎在所有东西上都加
@concurrent或nonisolated。对这类目标不要设置SWIFT_DEFAULT_ACTOR_ISOLATION,而是按旧的思路反过来,有意地去隔离。 - 一个以 actor 为主的并发系统。 如果你的设计确实在并行地运行许多东西(一条真正的流水线,而不是一个带几个后台任务的应用),那么 main-actor 默认就会跟你较劲。你想要的是显式的 actor 和 nonisolated 代码,而 SE-0466 的默认是错误的起点。
不过,对于一个应用而言,选择很简单:把两个设置都打开,让错误数坍缩,再把剩下的那一小撮当作一张地图——它精确地标出了你的代码在哪些地方离开了主线程。这张地图值得拥有。旧模型给你一千条警告却没有地图;新模型给你六条诚实的边界,以及一个终于与应用实际运行方式相符的默认。
最后说一句噪声与信号的事。当 Xcode 重建索引时,SourceKit 会在编辑器里显示跨文件的索引错误(”Cannot find type X in scope”、”No such module”),尤其是在刚刚重新生成一个项目之后。那些是索引产物,不是并发错误。只要 xcodebuild 报告 BUILD SUCCEEDED,并发模型就已满足,编辑器只是在追赶而已1。追逐索引幽灵,是在一次本已奏效的迁移上浪费一个下午最快的方式。
常见问题
我该如何在 Swift 6.2 中启用”默认主线程”的并发?
设置两个构建设置,在 Xcode 26 的新项目中都已默认开启:SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 和 SWIFT_APPROACHABLE_CONCURRENCY = YES5。在 Swift 包中,你通过 swiftSettings 设置等效的 upcoming-feature 标志,而不是使用 Xcode 的构建设置6。
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 究竟改变了什么?
它让一个模块的代码默认成为 main-actor-isolated(SE-0466),于是一个视图、它的模型和它的辅助代码无需任何注解就全都是 @MainActor2。并发于是成了你伸手去取的具名例外,而不再是你要在每一行上加注解去防范的弥漫性状态。
@concurrent 是什么,我什么时候该用它?
@concurrent 把一个 async 函数有意地卸载到全局执行器上,远离它调用方的 actor4。在 main-actor 默认下,你用它来处理那些经测量确实需要后台线程的工作(解码一个大负载、缩放一张图片);它与 @MainActor 互斥,所以一个函数要么在它的调用方所在之处运行,要么有意地从那里跑开。
为什么我会得到 “Main actor-isolated … can not be referenced from a nonisolated context”?
在 main-actor 默认下,纯函数、单例静态属性和常量会继承 main-actor 隔离,随后在从 nonisolated 上下文(例如一次默认参数求值)调用时报错。修法是在该成员上加一个精确的 nonisolated 注解,而不是一次压制;该值可以安全地共享,因为它不触及任何 actor 状态7。
我应该为库或框架目标启用”默认主线程”吗?
不应该。一个网络层、解析器或数据引擎没有理由默认采用 main actor,这样做会逼着你几乎在所有东西上都加 @concurrent 或 nonisolated。对非 UI 目标不要设置 SWIFT_DEFAULT_ACTOR_ISOLATION,而是按旧的思路反过来,有意地去隔离。
在 Swift 6.2 中,await 一个 nonisolated 的 async 函数还会跳转 actor 吗?
不会。SE-0461 让一个 nonisolated 的 async 函数在调用它的那个 actor 上运行,而不是跳到全局执行器上,因此 await 它不会再把你拖过一条隔离边界,也不会要求作用域内的一切都满足 Sendable3。
-
作者在 941 系列 iOS 应用(Ki、Return、Get Bananas)中的生产代码,全部以
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor和SWIFT_APPROACHABLE_CONCURRENCY = YES发布。下文的六个错误模式及其修法,就是 Ki 1.0.0 完整的严格并发清理集。 ↩↩↩ -
Swift Evolution,SE-0466: Control default actor isolation inference。让一个模块默认采用
@MainActor隔离,于是 UI 和应用目标都运行在 main actor 上,除非代码通过@concurrent或一个显式 actor 退出。 ↩↩↩ -
Swift Evolution,SE-0461: Run nonisolated async functions on the caller’s actor by default。一个 nonisolated 的
async函数默认为nonisolated(nonsending),在调用方的 actor 上运行,而不是跳到全局执行器上,这就消除了随那次跳转而来的跨边界Sendable要求。 ↩↩↩ -
Swift Evolution,SE-0461 引入了
@concurrent属性,让一个函数选择在全局执行器(一个后台线程)上运行。@concurrent和nonisolated(nonsending)是一个 nonisolated async 函数的两种对立隔离模式:一个函数要么在它的调用方所在之处运行,要么有意地从那里跑开。@concurrent不能与@MainActor或自定义全局 actor 组合使用。 ↩↩↩↩ -
SWIFT_APPROACHABLE_CONCURRENCY是总开关式的 Xcode 构建设置,用于启用 approachable-concurrency 的 upcoming 特性(包括 SE-0461 行为),而SWIFT_DEFAULT_ACTOR_ISOLATION用于选择模块的默认隔离。Xcode 26 的新项目以 main-actor 默认开启这两项。记载于 Donny Wals,“Exploring concurrency changes in Swift 6.2”,以及 Paul Hudson,“What’s new in Swift 6.2”,两者均与底层提案 SE-0461 和 SE-0466 交叉核对。 ↩↩↩ -
Swift,Swift Concurrency Migration Guide,”Enabling Complete Concurrency Checking” 及语言模式配置。在 Swift 包中,默认隔离和 approachable-concurrency 特性是通过
swiftSettings的 upcoming-feature 标志来设置的,而不是 Xcode 的构建设置。 ↩↩ -
Swift,Migration Guide: global actor isolation and
nonisolated。一个@MainActor类型的方法会继承 main-actor 隔离;nonisolated让一个方法退出,这对于不触及任何隔离状态的纯函数是正确的做法。 ↩↩ -
Apple Developer,
MainActor.assumeIsolated(_:)。断言当前执行已经在 main actor 上,并同步运行闭包而不发生 actor 跳转。如果该不变量不成立,断言会在运行时陷入(trap),所以它只在调用方保证位于主线程上的地方才有效。 ↩ -
Apple,WWDC 2026 Swift Group Lab。归于 Swift 核心团队的 Holly 和 Doug 的言论,转述自一份本地转录的录音;Apple 没有为该场次发布官方文字记录或字幕,因此措辞为近似表述,而非逐字引用。 ↩