文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载导读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 CodeTop-level global variables are implicitly assigned aMainActorglobal 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 checkingpreconcurrency允许旧模块在尚未补齐并发注解时被严格检查的新代码使用同时保证当上游模块后来补上注解、暴露出你的代码确有并发缺陷时诊断会以警告形式回归而不是直接让构建失败。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, orMainActor-isolate it, or mark it withnonisolated(unsafe).即遇到不合格的全局var时按优先级选择改成let如果该变量初始化后不再变化这是最优解——不可变 Sendable即可在任何上下文安全访问MainActor隔离如果确实需要可变全局状态且访问天然集中在主线程/主 actor显式声明隔离nonisolated(unsafe)标注仅在确有外部同步机制锁、原子、专门串行队列时使用作为最后手段。SE-0486 还说明这类调整可以被工具全自动执行在迁移模式下编译器输出带 fix-it 的警告一键应用即可保持行为不变从而把开发者精力留给真正需要人工判断行为变更的场景。4.2 语言模式与启用开关根据提案头部元数据状态ImplementedSwift 5.10Upcoming Feature FlagGlobalConcurrency在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 closuresSendable协议全局变量Sendable 类型判定的基础SE-0306 Actorsactor 隔离模型全局 actor 隔离机制的根基SE-0316 Global actorsMainActor等全局 actor提供隔离到全局 actor这一合法路径SE-0337 Incremental migration to concurrency checkingpreconcurrency、严格/最小检查模式preconcurrency import的来源与语义SE-0343 Concurrency in top-level code顶层变量隐式MainActor顶层全局变量的豁免依据SE-0486 Adoption tooling for Swift features特性迁移模式与 fix-itGlobalConcurrency的自动化迁移路线结语SE-0412 用一条极简规则全局变量要么隔离到全局 actor要么不可变且Sendable堵住了并发安全模型中最难以绕开的漏洞同时通过nonisolated(unsafe)、preconcurrency import和顶层代码豁免为存量代码与跨语言互操作保留了务实的逃生通道。对正在迁移到 Swift 6 严格并发模式的团队而言把全局可变状态收敛为let常量或显式的MainActor隔离是收益最高、风险最低的第一步nonisolated(unsafe)则应在同步原语齐备的前提下谨慎使用。赞分享文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载相关推荐swift-evolution 提案解读SE-0423 动态 Actor 隔离检查从非严格并发上下文加固 Swift 6 数据竞争安全swift evolution 提案解读SE 0423 动态 Actor 隔离检查从非严格并发上下文加固 Swift 6 数据竞争安全 SE 0423Dy文档Swift 6.2 并发增强 SE-0471 完全指南SerialExecutor 的 isIsolatingCurrentContext 自定义隔离检查Swift 6.2 并发增强 SE 0471 完全指南SerialExecutor 的 isIsolatingCurrentContext 自定义隔离检查 S文档Swift 全局 ActorGlobal Actors深入解析从 SE-0316 到 MainActor 的并发隔离实践Swift 全局 ActorGlobal Actors深入解析从 SE 0316 到 MainActor 的并发隔离实践 导读 全局 ActorGlob文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
