- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
导读
Swift 5.10 引入了 SE-0412「Strict concurrency for global variables(全局变量的严格并发检查)」,为全局变量和静态成员变量这类"静态存储"定义了无数据竞争的合法使用方式:要么隔离到全局 actor(如@MainActor),要么同时满足"不可变 +Sendable类型"。本指南以 SE-0412 提案原文 为骨架,结合仓库内相关提案(SE-0302、SE-0306、SE-0316、SE-0337、SE-0343)与迁移工具链文档(SE-0486),讲解规则细节、nonisolated(unsafe)逃生舱、@preconcurrency import互操作策略及迁移路径。读完本文,你将能理解全局变量并发检查的完整模型,并能把存量全局状态代码平稳迁移到 Swift 6 严格并发模式。
一、背景:为什么全局变量是并发检查的难点
在 Swift 并发模型中,隔离性(isolation)是防止数据竞争的核心手段。SE-0412 指出,全局变量之所以棘手,是因为"全局状态是任何程序上下文都能访问的内存",它绕过了其它所有隔离手段:
- 未被捕获的局部变量只能从该局部上下文访问,天然被隐式隔离;
- 值类型(struct/enum)的存储属性已被独占访问规则(exclusivity rules)隔离;
- 引用类型(class)的存储属性可以通过
Sendable强制约束或 actor 限制,隔离其所在对象。
但全局变量可以从任何地方读写,上述工具全部失效。提案给出了最小化的触发示例:
var value = 1 func f() { value = 2 // warning: reference to var 'value' is not concurrency-safe because it involves shared mutable state }这里value是一个全局可变变量,任何线程、任何 actor 上下文都可能同时读写它,编译器在严格并发检查下会直接给出诊断。
值得注意的是,全局变量与let常量(不可变存储)也属于本提案的管辖范围,因为"静态存储"(storage of static duration)包括全局作用域和静态成员中的let与存储型var两类。
二、解决方案:两条合法路径
SE-0412 提出的规则非常简洁:在严格并发检查(strict concurrency checking)下,每个全局变量必须满足以下二者之一:
- 隔离到某个全局 actor(如
@MainActor),或 - 同时满足"不可变(immutable)"且"类型为
Sendable"。
不可变且Sendable的全局变量可以从任何上下文安全访问(因为它既不会变化、值又可以在隔离域间安全传递);否则就必须借助全局 actor 提供同步串行化。
例如,把前面的示例改为let常量即可合法通过检查:
let value = 1 // 不可变,若 Int 为 Sendable,则全局合法 func f() { // 只能读取,不能写入 print(value) }而如果确实需要全局可变状态,则应当显式隔离:
@MainActor var globalTextSize: Int // 隔离到主 actor func notOnTheMainActor() async { globalTextSize = 12 // error: 仅主 actor 可同步访问 await MainActor.run { globalTextSize = 12 // 通过 await 跳转到主 actor 后合法 } }上面的@MainActor隔离语义来自 SE-0316 Global actors:全局 actor 是由某个类型标识的全局唯一 actor,任何声明都可以通过在该类型上加属性(attribute)声明自己隔离到该全局 actor,此后所有常规的 actor 隔离限制都生效——同步访问仅限同一全局 actor,跨 actor 访问需要异步跳转。
顶层代码(top-level code)的特殊豁免
本提案明确指出:顶层全局变量已被隐式隔离到@MainActor,因此自动满足新要求。这一机制来自 SE-0343 Concurrency in Top-level Code:
Top-level global variables are implicitly assigned a
@MainActorglobal actor isolation to prevent data races.
在 Swift 5 语言模式下,顶层变量还隐式携带@preconcurrency以减小源码破坏;进入 Swift 6 语言模式后,隔离检查完全生效——例如顶层变量a被一个未隔离到主 actor 的函数bar读取会报错。
三、详细设计:类型检查器层面的强制与逃生舱
3.1 在声明时检查
这些要求的执行点位于类型检查器(type checker)的声明阶段,即编译器在解析全局变量声明时就判断其是否满足"不可变 + Sendable"或"全局 actor 隔离"二者之一,而不是等到使用点才报错。这意味着错误会精确指向声明本身,便于开发者第一时间修正。
3.2 惰性初始化天然线程安全
全局变量(以及静态存储)在 Swift 中是惰性初始化的。本提案特别说明:虽然要求全局变量满足上述两条规则之一,但其初始化本身已经被保证线程安全,因此在严格并发检查下无需额外规定。也就是说,即使某个全局变量的初始化过程相对复杂,你也不必为"首次访问时的并发初始化"担忧——这一保证是语言层面的既有行为。
3.3 逃生舱:nonisolated(unsafe)
某些场景下,开发者希望放弃静态检查,依靠自己的数据隔离手段(例如用一个全局锁串行化所有访问)。SE-0412 提供了nonisolated(unsafe)属性来标注全局变量(或任何形式的存储),从而关闭对该变量的静态数据隔离检查:
nonisolated(unsafe) var global: String但提案同时给出重要警示:关闭静态检查后,如果同步机制实现不正确,运行时的动态分析(如独占访问规则、Thread Sanitizer)仍可能发现数据竞争。也就是说nonisolated(unsafe)是"把安全责任转移给开发者",而不是"消除竞争"。
SE-0458 Strict memory safety 将nonisolated(unsafe)明确归类为 Swift 的"不安全构造"(与标准库的Unsafe指针类型、与 C 语言的互操作并列),并指出:"Uses ofnonisolated(unsafe)entities are not memory-safe."因此在生产代码中应将其视为最后手段,并配合锁、原子操作等同步原语使用。
3.4 局部变量上的nonisolated(unsafe)
同一注解也可用于局部变量,以抑制该局部变量被异步引用时产生的静态诊断。提案给出的示例:
func f() async { nonisolated(unsafe) var value = 1 let task = Task { value = 2 return value } print(await task.value) }没有该标注时,局部var被Task闭包捕获并在异步任务中修改,会触发"共享可变状态"类诊断;加上nonisolated(unsafe)后开发者自行保证同步,诊断被抑制。这正是 SE-0434 Global actor-isolated types usability 中提及的典型用法——该文档展示了隔离到全局 actor 的结构体存储属性在实现协议时,同样只能靠nonisolated(unsafe) var来绕开限制:
nonisolated(unsafe) var x: Int = 03.5nonisolated(unsafe)的语法歧义解析
由于nonisolated是上下文关键字(contextual keyword),在脚本模式下,当nonisolated(unsafe)单独占一行、紧跟在顶层变量声明之前时,存在歧义:它也可能被解析为调用一个名为nonisolated、带一个无标签参数unsafe的函数。
SE-0412 给出的消歧规则是:如果nonisolated只有一个无标签参数unsafe且紧跟变量声明,则优先将其解释为关键字(即隔离规格说明)。这一消歧会破坏极少数依赖"调用名为nonisolated的函数"的顶层脚本,但属于可接受的边缘破坏(详见后文"源码兼容性")。
3.6 跨模块互操作:@preconcurrency import
导入模块时使用@preconcurrency import,可以抑制对"缺少显式并发注解的导入全局变量"进行数据隔离检查时可能产生的错误;而任何对@preconcurrency导入模块中并发不安全的全局变量的使用点,会产生一条警告(而非错误)。
这一机制源自 SE-0337 Incremental migration to concurrency checking:@preconcurrency允许旧模块在尚未补齐并发注解时被严格检查的新代码使用,同时保证"当上游模块后来补上注解、暴露出你的代码确有并发缺陷"时,诊断会以警告形式回归,而不是直接让构建失败。
@preconcurrency import LegacyModule // 抑制导入全局变量的隔离检查错误 func useIt() { LegacyModule.globalVar = 1 // 仅产生 warning,而非 error }3.7 其它语言导入的全局变量
来自其它语言(C/Objective-C 等)的导入默认视为@preconcurrency。对于这些全局变量,仍有工具可以保证安全:
- 使用
__attribute__((swift_attr("@MainActor")))(C 或 Objective-C 中)将全局变量隔离到主 actor; - 或将访问包装在声明了正确隔离/加锁的、更安全的 API 内部。
这为系统级全局状态(如 C 库中的全局配置、缓存指针等)提供了一条明确的升级路径。
四、迁移实战:把存量全局变量改到合法状态
4.1 三条规范化修改路线
SE-0486 Adoption tooling for Swift features 的"自动化"一节,明确列出了GlobalConcurrency特性的三条机械迁移路径:
[
GlobalConcurrency][SE-0412]: Convert the global variable to alet, or@MainActor-isolate it, or mark it withnonisolated(unsafe).
即遇到不合格的全局var时,按优先级选择:
- 改成
let:如果该变量初始化后不再变化,这是最优解——不可变 +Sendable即可在任何上下文安全访问; @MainActor隔离:如果确实需要可变全局状态且访问天然集中在主线程/主 actor,显式声明隔离;nonisolated(unsafe)标注:仅在确有外部同步机制(锁、原子、专门串行队列)时使用,作为最后手段。
SE-0486 还说明,这类调整可以被工具全自动执行(在迁移模式下编译器输出带 fix-it 的警告,一键应用即可保持行为不变),从而把开发者精力留给真正需要人工判断行为变更的场景。
4.2 语言模式与启用开关
根据提案头部元数据:
- 状态:Implemented(Swift 5.10);
- Upcoming Feature Flag:
GlobalConcurrency,在Swift 6 语言模式下默认启用; - 实现位于
main分支,受-enable-experimental-feature GlobalConcurrency门控。
因此你可以在 Swift 5.10 及以后版本中,通过-enable-upcoming-feature GlobalConcurrency(或旧工具链上的 experimental 开关)提前开启该检查,或在swift-tools-version声明为 6.0+ 的包中直接获得完整错误诊断。严格并发检查的完整背景(Sendable强制、@preconcurrency行为、警告/错误分级)见 SE-0337。
五、兼容性影响评估
5.1 源码兼容性
由于新增了限制,启用严格并发检查时,部分类型声明可能需要修改(如上述三种迁移路线)。但这些源码改动对"任何带并发特性的 Swift 版本"仍然是向后兼容的(例如把var改成let或加@MainActor,在 Swift 5.5+ 均可编译)。
唯一的破坏点来自 3.5 节的消歧规则:顶层脚本中"调用名为nonisolated、带单个无标签参数unsafe的函数且紧跟变量声明"的旧代码会失效,因为该写法会被优先解释为隔离规格。
5.2 ABI 兼容性
本提案本身不新增也不影响 ABI。但采用方因规则而修改类型声明(如为存储加隔离属性)时,可能间接影响该项目的 ABI——例如在库边界上暴露了 actor 隔离的函数,其调用约定相关 mangled 名称可能变化(参见 SE-0337 对"并发注解参与函数名 mangling"的讨论)。
5.3 采用影响
对于一个正在采用严格并发检查的项目,需要盘点所有全局/静态存储,逐一声明其并发策略(let/ 全局 actor /nonisolated(unsafe)/@preconcurrency import)。这是一次性但全局性的审计,建议结合迁移模式(migration mode)先以警告形式暴露全部问题,再分批修复。
六、备选方案与设计取舍
SE-0412 的 Alternatives considered 一节记录了三个被否决的方向,理解它们有助于把握规则的边界:
6.1 隐式加锁(implicit locking)
一种想法是:对所有需要隔离的全局变量,在每次访问时隐式加锁。这虽然能提供内存安全,但对线程安全有害——开发者很容易写出非原子的使用模式:
// global 的值可能在"读取乘法表达式"与"赋值写入"之间被并发修改 global = global * 2即使编译器为单次读写加锁,read-modify-write这种复合操作也无法在单次锁内原子完成,竞争依旧。提案补充道:隐式加锁对非Sendable类型也行不通(除非强制值在访问期间保持隔离),而这需要依赖"安全发送非 Sendable 值跨隔离域"这类过于高级的特性,不适合作为基础问题的解法。此外,旧语言模式的一贯立场就是"并发不安全",因此不需要为了源码兼容而引入隐式锁。
6.2 默认全部@MainActor
另一个方案是把所有需要隔离的全局变量默认归到@MainActor。提案认为让开发者思考选择是更好的做法——例如某个全局状态或许本就应该改成let常量,默认主 actor 会掩盖这种更优的建模机会。
6.3 基于访问控制的全局分析
理论上,访问控制可用于推断安全性:例如某全局变量是文件私有(private/fileprivate),且该文件内所有访问都处于单一全局 actor 上下文,或该变量从不被写入,则可以推断其并发安全。但这是比编译器通常愿意做的更全局的分析——必须检查上下文中的所有内容,且结果难以被开发者理解("为什么它能通过?")。因此被否决,改为显式规则。
七、未来方向:全局 actor 推断
SE-0412 认为,隔离到全局 actor不一定要显式写出,存在推断空间:
A global mutable variable of global-actor-constrained type could be inferred to be constrained to that global actor.
例如:一个类型被约束到某全局 actor 的全局可变变量,可以推断为同样约束到该全局 actor(如果变量不可变则无此必要,因为全局 actor 约束的 class 类型本身是Sendable的)。这为未来减少冗余标注、让GlobalConcurrency更易用留出了演进余地。
八、修订历史与关键演进
提案的 revision history 记录了两处评审后的重要变更,值得实践者注意:
- 移除了"C 全局变量隐式
nonisolated(unsafe)导入",改为以@preconcurrency import作为抑制全局变量静态隔离检查的机制(即 3.6 节的方案); - 澄清了
nonisolated(unsafe)用于局部变量的语义(即 3.4 节)。
这两处修订说明:在严格并发世界里,跨语言全局变量的默认处理被收敛为"显式声明互操作策略",而非静默放行。
九、相关提案速查
| 提案 | 主题 | 与 SE-0412 的关系 |
|---|---|---|
| SE-0302 Concurrent values and concurrent closures | Sendable协议 | 全局变量"Sendable 类型"判定的基础 |
| SE-0306 Actors | actor 隔离模型 | 全局 actor 隔离机制的根基 |
| SE-0316 Global actors | @MainActor等全局 actor | 提供"隔离到全局 actor"这一合法路径 |
| SE-0337 Incremental migration to concurrency checking | @preconcurrency、严格/最小检查模式 | @preconcurrency import的来源与语义 |
| SE-0343 Concurrency in top-level code | 顶层变量隐式@MainActor | 顶层全局变量的豁免依据 |
| SE-0486 Adoption tooling for Swift features | 特性迁移模式与 fix-it | GlobalConcurrency的自动化迁移路线 |
结语
SE-0412 用一条极简规则("全局变量要么隔离到全局 actor,要么不可变且Sendable")堵住了并发安全模型中最难以绕开的漏洞,同时通过nonisolated(unsafe)、@preconcurrency import和顶层代码豁免,为存量代码与跨语言互操作保留了务实的逃生通道。对正在迁移到 Swift 6 严格并发模式的团队而言,把全局可变状态收敛为let常量或显式的@MainActor隔离,是收益最高、风险最低的第一步;nonisolated(unsafe)则应在同步原语齐备的前提下谨慎使用。
- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
相关推荐
swift-evolution 提案解读:SE-0423 动态 Actor 隔离检查,从非严格并发上下文加固 Swift 6 数据竞争安全
swift evolution 提案解读:SE 0423 动态 Actor 隔离检查,从非严格并发上下文加固 Swift 6 数据竞争安全 SE 0423(Dy
文档Swift 6.2 并发增强 SE-0471 完全指南:SerialExecutor 的 isIsolatingCurrentContext 自定义隔离检查
Swift 6.2 并发增强 SE 0471 完全指南:SerialExecutor 的 isIsolatingCurrentContext 自定义隔离检查 S
文档Swift 全局 Actor(Global Actors)深入解析:从 SE-0316 到 MainActor 的并发隔离实践
Swift 全局 Actor(Global Actors)深入解析:从 SE 0316 到 MainActor 的并发隔离实践 导读 全局 Actor(Glob
文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考