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