理解 Swift Actor 隔离关键字:nonisolated 和 isolated

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

bankaccountHolder 都是不可变属性,因此我们可以显式地将计算属性标记为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

你可能感兴趣的:(理解 Swift Actor 隔离关键字:nonisolated 和 isolated)