理解 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 中的 Actor 是同步訪問共享可變狀態的好方法。然而,在某些情況下,我們希望控 Actor 隔離,因爲我們可能確定只訪問不可變狀態。通過使用非隔離(nonisolated)和隔離(isolated)關鍵字,我們可以精確控制Actor的隔離狀態。

轉自 Nonisolated and isolated keywords: Understanding Actor isolation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章