Swift 中的 Sendable 和 @Sendable

2,116 阅读3分钟

前言

在 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 保持一致,但我们也可以自己手动添加支持。