SE-313 引入了非隔离(nonisolated)和隔离(isolated)关键字作为添加 Actor 隔离控制的一部分。 Actor 是一种使用新并发框架为共享可变状态提供同步的新方法。
如果您不熟悉 Swift 中的 Actor,我鼓励您阅读我的文章Swift中的Actors 使用以如何及防止数据竞争,文章内详细描述了它。本文将解释在 Swift 中使用 Actor 时如何控制方法和参数的隔离。
了解Actor的默认行为
默认情况下,actor 的每个方法都是隔离的,这意味着您必须已经在 actor 的上下文中,或者使用 await 等待批准访问 actor 包含的数据。
您可以在我的文章 Swift 中的async/await ——代码实例详解了解有关 async/await 的更多信息。
通常我们使用Actor会遇到以下错误:
- Actor-isolated property ‘balance’ can not be referenced from a non-isolated context
- Expression is ‘async’ but is not marked with ‘await’
这两个错误都有相同的根本原因:Actor 隔离对其属性的访问以确保互斥访问。
以如下银行账户 Actor 为例:
actor BankAccountActor {
enum BankError: Error {
case insufficientFunds
}
var balance: Double
init(initialDeposit: Double) {
self.balance = initialDeposit
}
func transfer(amount: Double, to toAccount: BankAccountActor) async throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
await toAccount.deposit(amount: amount)
}
func deposit(amount: Double) {
balance = balance + amount
}
}
Actor 方法默认是隔离的,但没有明确标记为隔离。您可以将此与默认情况下为内部但未使用 internal
关键字标记的方法进行比较。实际上真实代码大概如下所示:
isolated func transfer(amount: Double, to toAccount: BankAccountActor) async throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
await toAccount.deposit(amount: amount)
}
isolated func deposit(amount: Double) {
balance = balance + amount
}
但是,像这个例子一样使用隔离关键字(isolated)显式标记方法将导致以下错误:
‘isolated’ may only be used on ‘parameter’ declarations
我们只能在参数声明中使用隔离关键字。
将 Actor 参数标记为隔离
对参数使用隔离关键字可以很好地使用更少的代码来解决特定问题。上面的代码示例介绍了一个deposit
方法来更改另一个银行账户的余额:
func transfer(amount: Double, to toAccount: isolated BankAccountActor) async throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
toAccount.balance += amount
}
结果是使用更少的代码同时可能使您的代码更易于阅读。
编译器目前禁止但允许使用多个隔离参数:
func transfer(amount: Double, from fromAccount: isolated BankAccountActor, to toAccount: isolated BankAccountActor) async throws {
// ..
}
不过,最初的提议表明这是不允许的,因此未来的 Swift 版本可能会要求您更新此代码。
在 Actor 中使用 nonisolated 关键字
将方法或属性标记为非隔离可用于选择退出Actor的默认隔离。在访问不可变值或符合协议要求时,选择退出可能会有所帮助。
在以下示例中,我们为Actor添加了一个帐户持有人姓名:
actor BankAccountActor {
let accountHolder: String
// ...
}
帐户持有人是不可变的,因此可以安全地从非隔离环境访问。编译器足够聪明,可以识别这种状态,因此无需显式将此参数标记为非隔离。
但是,如果我们引入计算属性访问不可变属性,我们必须帮助编译器识别这一点。让我们看一下下面的例子:
actor BankAccountActor {
let accountHolder: String
let bank: String
var details: String {
"Bank: \(bank) - Account holder: \(accountHolder)"
}
// ...
}
如果我们现在要打印出detail
,我们会遇到以下错误:
Actor-isolated property ‘details’ can not be referenced from a non-isolated context
bank
和 accountHolder
都是不可变属性,因此我们可以显式地将计算属性标记为nonisolated
然后便可以解决错误:
actor BankAccountActor {
let accountHolder: String
let bank: String
nonisolated var details: String {
"Bank: \(bank) - Account holder: \(accountHolder)"
}
// ...
}
使用非隔离解决协议一致性
同样的原则也适用于添加协议一致性,在这种一致性中,您确定只能访问不可变状态。例如,我们可以用更好的 CustomStringConvertible
协议替换 details
属性:
extension BankAccountActor: CustomStringConvertible {
var description: String {
"Bank: \(bank) - Account holder: \(accountHolder)"
}
}
使用 Xcode 推荐的默认实现,我们会遇到以下错误:
Actor-isolated property ‘description’ cannot be used to satisfy a protocol requirement
我们可以再次通过使用 nonisolated
关键字解决这个问题:
extension BankAccountActor: CustomStringConvertible {
nonisolated var description: String {
"Bank: \(bank) - Account holder: \(accountHolder)"
}
}
如果我们在非隔离环境中意外访问了隔离属性,编译器将足够聪明地警告我们:
从非隔离环境访问隔离属性将导致编译器错误。
继续您的 Swift 并发之旅
并发更改不仅仅是 async-await,还包括许多您可以在代码中受益的新功能。所以当你在做的时候,为什么不深入研究其他并发特性呢?
- Swift 中的 async/await
- Swift 中的 async let
- Swift 中的 Task
- Swift 中的 Actors 使用以如何及防止数据竞争
- Swift 中的 MainActor 使用和主线程调度
- 理解 Swift Actor 隔离关键字:nonisolated 和 isolated
- Swift 中的 Sendable 和 @Sendable 闭包
- Swift 中的 AsyncThrowingStream 和 AsyncStream
- Swift 中的 AsyncSequence
结论
Swift 中的 Actor 是同步访问共享可变状态的好方法。然而,在某些情况下,我们希望控 Actor 隔离,因为我们可能确定只访问不可变状态。通过使用非隔离(nonisolated
)和隔离(isolated
)关键字,我们可以精确控制Actor的隔离状态。
转自 Nonisolated and isolated keywords: Understanding Actor isolation