前言
在 Swift 5.5 的版本的发布版本中,Apple 引入了多项对于并发性改进,其中 Sendable 和 @Sendable 是两个核心概念,它们解决了结构化的并发结构体和 actor 消息之间传递的类型检查问题。在 Swift 开发中,这两个特性对于编写安全、高效的并发代码是非常重要的。
什么是 Sendable 协议
Sendable 协议是 Swift 并发模型中的一个关键协议,它向编译器表明某个类型是否可以被安全的跨并发域(如 actors、并发队列等)传递。当类型符合 Sendable 协议时,编译器可以保证该类型的实例在并发环境下是线程安全的。
标准库中的许多类型都已经默认支持了 Sendable 协议,这样就避免了我们手动为系统类型添加 Sendable 协议的支持。
例如字符串类型:
extension String: Sendable {}
假设我们的自定义结构体只包含遵守了 Sendable 协议的基本类型,那该结构体的实力也是隐式的遵守了 Sendable 协议:
struct Animal {
var name: String
}
但如果是类的话,就没有这个特性了:
class Animal {
var name: String
}
类之所以不符合规则,因为它是引用类型,对其他并发域是可变的。换句话说,类不是线程安全的,不能传递,所以编译器不能隐式地将其标记为 Sendable。
如何使用 Sendable 协议
虽然系统的 Sendable 协议隐式支持省去了许多需要我们自己为类型添加 Sendable 协议的情况。但是在某些情况下,当我们知道我们的类型是线程安全的时候,编译器不会对其进行隐性支持。
没有隐式支持 Sendable 协议,但可以手动标记为可发送的类型常见示例是不可变类:
final class Animal: Sendable {
let name: String
init(name: String) {
self.name = name
}
}
因为 Animal 类是不可改变的(immutable),因此它是线程安全的,所以我们可以显示的让其遵守 Sendable 协议。
还有具有内部加锁机制的类:
extension DispatchQueue {
static let animalMutatingLock = DispatchQueue(label: "animal.lock.queue")
}
final class MutableAnimal: @unchecked Sendable {
private var name: String = ""
func updateName(_ name: String) {
DispatchQueue.animalMutatingLock.sync {
self.name = name
}
}
}
如何使用 @Sendable
函数可以跨并发域传递,因此也需要可发送的一致性。然而,函数不能遵循协议,所以 Swift 引入了 @Sendable 属性。可以传递的函数示例包括全局函数声明、闭包以及 getter 和 setter 等访问器。
使用 @Sendable属性,我们将告诉编译器它不需要额外的同步操作,因为闭包中捕获的所有值都是线程安全的。一个典型的例子是在 Actor 隔离中使用闭包:
actor AnimalFilter {
func filteredAnimal(_ isIncluded: @Sendable (Animal) -> Bool) async -> [Animal] { }
}
如果你使用非可发送类型的闭包,我们会遇到一个错误:
let listOfAnimals = AnimalFilter()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredAnimals = await listOfAnimals.filteredAnimal { animal in
// Error: Reference to captured var 'searchKeyword' in concurrently-executing code
guard let searchKeyword = searchKeyword else { return false }
return animal.name == searchKeyword.string
}
当然,我们可以通过使用 String 来快速解决这个问题,但它演示了编译器如何帮助我们加强线程安全。
总结
Sendable 协议和函数的 @Sendable 属性使得在 Swift 中处理并发时告诉编译器线程是安全的。它们提供一种机制来隔离并发程序中的状态,以消除数据竞争。在许多情况下,编译器将帮助我们隐式地与 Sendable 保持一致,但我们也可以自己手动添加支持。