Swift 5.10 发布
Swift 的设计目标是默认安全,在编译时防止整类编程错误。C 语言系列中未定义行为的来源,例如在使用变量之前对其进行初始化或使用后释放,在 Swift 中都被明确避免。
并发代码中一个日益重要的未定义行为来源是,当一个线程正在写入同一内存时,另一个线程无意中访问了该内存。这种不安全类型被称为数据竞争,数据竞争使并发程序难以正确编写。Swift 通过参与者和任务提供的数据隔离来解决这个问题,这保证了对共享可变状态的互斥访问。自 2020 年 Swift 并发路线图发布以来,数据隔离强制执行一直在积极开发中。
Swift 5.10 完成了并发语言模型中的完整数据隔离。 这个重要的里程碑经历了多年在多个版本上的积极开发。并发模型在 Swift 5.5 中引入,包括 async/await、参与者和结构化并发。Swift 5.7 引入了 Sendable 作为线程安全类型的基本概念,其值可以在任意并发上下文之间共享,而不会引入数据竞争的风险。现在,在 Swift 5.10 中,当启用完整的并发检查选项时,将在语言的所有领域强制执行完整的数据隔离。
Swift 5.10 中的完整数据隔离为下一个主要版本 Swift 6 奠定了基础。Swift 6.0 编译器将提供一种新的、可选择加入的 Swift 6 语言模式,该模式将默认强制执行完整的数据隔离,我们将着手过渡以消除所有用 Swift 编写的软件中的数据竞争。
在某些情况下,Swift 5.10 会产生数据竞争警告,在这些情况下,可以通过额外的编译器分析来证明代码是安全的。Swift 6 版本语言开发的一个主要重点是通过减轻经证实是安全的常见代码模式中的误报并发错误,来提高严格并发检查的可用性。
请继续阅读以了解 Swift 5.10 中的完整数据隔离、参与者隔离检查的新不安全选择退出以及 Swift 6 之前的剩余并发演化。
Swift 5.10 中的数据竞争安全性
完整数据隔离
Swift 5.10 在语言的各个方面完善了数据竞争安全语义,并修复了 Sendable 和参与者隔离检查中的大量错误,以加强完整并发检查的保证。当使用编译器标志 -strict-concurrency=complete 构建代码时,Swift 5.10 将在编译时诊断数据竞争的潜在可能性,除非使用了显式的不安全选择退出,例如 nonisolated(unsafe) 或 @unchecked Sendable。
例如,在 Swift 5.9 中,以下代码由于在参与者外部评估了 @MainActor 隔离的初始化器而在运行时断言隔离失败,但在 -strict-concurrency=complete 下未诊断出来
@MainActor
class MyModel {
private init() {
MainActor.assertIsolated()
}
static let shared = MyModel()
}
func useShared() async {
let model = MyModel.shared
}
await useShared()
上面的代码允许数据竞争。MyModel.shared 是一个 @MainActor 隔离的静态变量,它在首次访问时评估一个 @MainActor 隔离的初始值。MyModel.shared 是从 useShared() 函数内部的 nonisolated 上下文中同步访问的,因此初始值是在主参与者之外计算的。在 Swift 5.10 中,使用 -strict-concurrency=complete 编译代码会产生一个警告,指出访问必须异步完成
warning: expression is 'async' but is not marked with 'await'
let model = MyModel.shared
^~~~~~~~~~~~~~
await
解决数据竞争的可能修复方法是:1) 使用 await 异步访问 MyModel.shared;2) 使 MyModel.init 和 MyModel.shared 都为 nonisolated,并将需要主参与者的代码移动到单独的隔离方法中;或者 3) 将 useShared() 隔离到 @MainActor。
您可以在 Swift 5.10 发行说明中找到有关完整数据隔离编程模型的更改和添加的更多详细信息。
不安全的选择退出
不安全的选择退出(例如 @unchecked Sendable 一致性)对于沟通代码在编译器无法自动证明的情况下是数据竞争安全的非常重要。当同步以编译器无法推理的方式实现时(例如通过特定于操作系统的原语或在使用 C/C++/Objective-C 中实现的线程安全类型时),这些工具是必要的。但是,@unchecked Sendable 一致性很难正确使用,因为它们使整个类型都选择退出数据竞争安全检查。在许多情况下,类型中只需要一个特定的属性选择退出,而其余实现都遵循静态并发安全性。
Swift 5.10 引入了一个新的 nonisolated(unsafe) 关键字,用于选择退出存储属性和变量的参与者隔离检查。nonisolated(unsafe) 可以用于任何形式的存储,包括存储属性、局部变量和全局/静态变量。
例如,全局变量和静态变量可以从代码中的任何位置访问,因此它们必须是不可变的和 Sendable,或者隔离到全局参与者
import Dispatch
struct MyData {
static let cacheQueue = DispatchQueue(...)
// All access to 'globalCache' is guarded by 'cacheQueue'
static var globalCache: [MyData] = []
}
使用 -strict-concurrency=complete 构建上述代码时,编译器会发出警告
warning: static property 'globalCache' is not concurrency-safe because it is non-isolated global shared mutable state
static var globalCache: [MyData] = []
^
note: isolate 'globalCache' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'
globalCache 的所有用途都受到 cacheQueue.async { ... } 的保护,因此实际上此代码没有数据竞争。在这种情况下,可以将 nonisolated(unsafe) 应用于静态变量以消除并发警告
import Dispatch
struct MyData {
static let cacheQueue = DispatchQueue(...)
// All access to 'globalCache' is guarded by 'cacheQueue'
nonisolated(unsafe) static var globalCache: [MyData] = []
}
nonisolated(unsafe) 还消除了对 @unchecked Sendable 包装器类型的需求,这些类型仅用于在没有并发访问可能性的情况下在隔离边界之间传递非 Sendable 值的特定实例
// 'MutableData' is not 'Sendable'
class MutableData { ... }
func processData(_: MutableData) async { ... }
@MainActor func send() async {
nonisolated(unsafe) let data = MutableData()
await processData(data)
}
请注意,如果没有正确实现同步机制以实现数据隔离,来自排他性强制执行的动态分析或诸如 Thread Sanitizer 之类的工具可能仍会识别出故障。
Swift 6 之前的语言演化
Swift 的下一个版本将是 Swift 6。 Swift 5.10 中的完整并发模型过于严格,并且正在积极开发多个 Swift Evolution 提案,以通过消除误报数据竞争错误来提高完整数据隔离的可用性。这项工作包括 当编译器确定不存在并发访问的可能性时,解除对跨隔离边界传递非 Sendable 值的限制、更有效地推断函数和键路径的 Sendable 等等。您可以在 /swift-evolution 上找到将完善 Swift 6 的提案集。
后续步骤
尝试完整的并发检查
您可以通过在您的项目中试用完整的并发检查并提供关于您体验的反馈,来帮助塑造向 Swift 6 语言模式的过渡。
如果您发现任何剩余的编译器错误,其中完整的并发检查未在编译时诊断出数据竞争,请报告问题。
您还可以提供反馈,以帮助改进并发文档、编译器错误消息和即将发布的 Swift 6 迁移指南。如果您遇到编译器诊断出您不理解的数据竞争警告,或者您不确定如何解决给定的数据竞争警告,请使用 concurrency 标签在 Swift 论坛上发起讨论主题。
下载
Swift 5.10 的官方二进制文件可从 下载,适用于 macOS、Windows 和 Linux。
Swift Evolution 附录
以下语言提案已通过 Swift Evolution 流程并在 Swift 5.10 中实现
- SE-0327: 关于参与者和初始化
- SE-0383: 弃用 @UIApplicationMain 和 @NSApplicationMain
- SE-0404: 允许协议在非泛型上下文中嵌套
- SE-0411: 隔离的默认值表达式
- SE-0412: 全局变量的严格并发