一、前言
- Sendable 和 @Sendable 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。
二、使用 Sendable
① 在什么时候使用 Sendable?
- Sendable 协议和闭包表明那些传递的值的公共 API 是否线程安全的向编译器传递了值。当没有公共修改器、有内部锁定系统或修改器实现了与值类型一样的复制写入时,公共 API 可以安全地跨并发域使用。
- 标准库中的许多类型已经支持了 Sendable 协议,消除了对许多类型添加一致性的要求。由于标准库的支持,编译器可以为你的自定义类型创建隐式一致性。
- 例如,整型支持该协议:
extension Int: Sendable {}
- 一旦创建一个具有 Int 类型的单一属性的值类型结构体,就隐式地得到了对 Sendable 协议的支持。
// 隐式地遵守 Sendable 协议
struct Article {
var views: Int
}
- 与此同时,同样的 Article 内容的类,将不会有隐式遵守该协议:
// 不会隐式的遵守 Sendable 协议
class Article {
var views: Int
}
- 类不符合要求,因为它是一个引用类型,因此可以从其它并发域变异。换句话说,该类文章(Article)的传递不是线程安全的,所以编译器不能隐式地将其标记为遵守 Sendable 协议。
② 使用泛型和枚举时的隐式一致性
- 很好理解的是,如果泛型不符合 Sendable 协议,编译器就不会为泛型添加隐式的一致性。
// 因为 Value 没有遵守 Sendable 协议,所以 Container 也不会自动的隐式遵守该协议
struct Container {
var child: Value
}
- 然而,如果将协议要求添加到泛型中,将得到隐式支持:
// Container 隐式地符合 Sendable,因为它的所有公共属性也是如此
struct Container {
var child: Value
}
- 如果枚举值不符合 Sendable 协议,隐式的 Sendable 协议一致性就不会起作用。可以看到,自动从编译器中得到一个错误:
Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’
- 可以通过使用一个值类型 String 来解决这个错误,因为它已经符合 Sendable。
enum State: Sendable {
case loggedOut
case loggedIn(name: String)
}
③ 从线程安全的实例中抛出错误
- 同样的规则适用于想要符合 Sendable 的错误类型:
struct ArticleSavingError: Error {
var author: NonFinalAuthor
}
extension ArticleSavingError: Sendable { }
- 由于作者不是不变的(non-final),而且不是线程安全的,会遇到以下错误:
Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’
- 可以通过确保 ArticleSavingError 的所有成员都符合 Sendable 协议来解决这个错误。
三、如何使用 Sendable 协议
- 隐式一致性消除很多需要自己为 Sendable 协议添加一致性的情况。然而,在有些情况下,我们知道类型是线程安全的,但是编译器并没有添加隐式一致性。
- 常见的例子是被标记为不可变和内部具有锁定机制的类:
// User 是不可改变的,因此是线程安全的,所以可以遵守 Sendable 协议
final class User: Sendable {
let name: String
init(name: String) { self.name = name }
}
- 需要用 @unchecked 属性来标记可变类,以表明类由于内部锁定机制所以是线程安全的:
extension DispatchQueue {
static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}
final class MutableUser: @unchecked Sendable {
private var name: String = ""
func updateName(_ name: String) {
DispatchQueue.userMutatingLock.sync {
self.name = name
}
}
}
四、遵守 Sendable 的限制
- Sendable 协议的一致性必须发生在同一个源文件中,以确保编译器检查所有可见成员的线程安全。
- 例如,可以在例如 Swift package 这样的模块中定义以下类型:
public struct Article {
internal var title: String
}
- Article 是公开的,而标题 title 是内部的,在模块外不可见。因此,编译器不能在源文件之外应用 Sendable 一致性,因为它对标题属性不可见,即使标题使用的是遵守 Sendable 协议的 String 类型。
- 同样的问题发生在我们想要使一个可变的非最终类遵守 Sendable 协议时:
- 由于该类是非最终的,无法符合 Sendable 协议的要求,因为不确定其他类是否会继承 User 的非 Sendable 成员。因此,会遇到以下错误:
Non-final class ‘User’ cannot conform to Sendable; use @unchecked Sendable
- 正如所看到的,编译器建议使用 @unchecked Sendable,可以把这个属性添加到 User 类中,并摆脱这个错误:
class User: @unchecked Sendable {
let name: String
init(name: String) { self.name = name }
}
- 然而,这确实要求无论何时从 User 继承,都要确保它是线程安全的。由于我们给自己和同事增加了额外的责任,因此不鼓励使用这个属性,建议使用组合、最终类或值类型来实现目的。
五、如何使用 @Sendabele?
- 函数可以跨并发域传递,因此也需要可发送的一致性。然而,函数不能符合协议,所以 Swift 引入了 @Sendable 属性,可以传递的函数的例子是全局函数声明、闭包和访问器,如 getters 和 setters。
- 使用 @Sendable 属性,将告诉编译器,它不需要额外的同步,因为闭包中所有捕获的值都是线程安全的。例如,在 Actor isolation 中使用闭包:
actor ArticlesList {
func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
// ...
}
}
- 如果用非 Sendabel 类型的闭包,会遇到一个错误:
let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
// Error: Reference to captured var 'searchKeyword' in concurrently-executing code
guard let searchKeyword = searchKeyword else { return false }
return article.title == searchKeyword.string
}
- 当然,可以通过使用一个普通的 String 来快速解决这种情况,但它展示了编译器如何执行线程安全。
六、Swift 6 代码启用并发性检查
- Xcode 14 允许通过 SWIFT_STRICT_CONCURRENCY 构建设置启用严格的并发性检查。
- 这个构建设置控制编译器对 Sendable 和 actor-isolation 检查的执行水平:
-
- Minimal : 编译器将只诊断明确标有 Sendable 一致性的实例,并等同于 Swift 5.5 和 5.6 的行为,不会有任何警告或错误。
-
- Targeted: 强制执行 Sendable 约束,并对所有采用 async/await 等并发的代码进行 actor-isolation 检查,编译器还将检查明确采用 Sendable 的实例,这种模式试图在与现有代码的兼容性和捕捉潜在的数据竞赛之间取得平衡。
-
- Complete: 匹配预期的 Swift 6 语义,以检查和消除数据竞赛,这种模式检查其他两种模式所做的一切,并对你项目中的所有代码进行这些检查。
- 严格的并发检查构建设置有助于 Swift 向数据竞赛安全迈进,与此构建设置相关的每一个触发的警告都可能表明代码中存在潜在的数据竞赛。因此,必须考虑启用严格并发检查来验证代码。
- Enabling strict concurrency in Xcode 14,会得到的警告数量取决于在项目中使用并发的频率,对于 Stock Analyzer,有大约 17 个警告需要解决:
- 这些警告可能让人望而生畏,但利用本文的知识,可以摆脱大部分警告,防止数据竞赛的发生。然而,有些警告是无法控制的,因为是外部模块触发了它们。
在上述 SharedWithYou 框架的例子中,最好是等待库的所有者添加 Sendable 支持。在这种情况下,这就意味着要等待苹果公司为 SWHighlight 实例指明 Sendable 的一致性。对于这些库,可以通过使用 @preconcurrency 属性来暂时禁用 Sendable 警告:
@preconcurrency import SharedWithYou
- 重要的是要明白,并没有解决这些警告,而只是禁用了它们。来自这些库的代码仍然有可能发生数据竞赛。如果正在使用这些框架的实例,需要考虑实例是否真的是线程安全的。一旦使用的框架被更新为 Sendable 的一致性,可以删除 @preconcurrency 属性,并修复可能触发的警告。