文章系列:
Swift Actors是swift 5.5新引入的,作為對(duì)Concurrency最重要的特性變更,Actor試圖解決并行開發(fā)中常見的數(shù)據(jù)競爭問題。
什么是Actor
Actor的概念并不新鮮, Actor 模式是一個(gè)通用的并發(fā)編程模型,而非某個(gè)語言或框架所有,幾乎可以用在任何一門編程語言中,最早Actor模型(Actor model)首先是由Carl Hewitt在1973定義, 由Erlang OTP 推廣。Actor類似面向?qū)ο缶幊蹋∣O)中的對(duì)象,每個(gè)Actor實(shí)例封裝了自己相關(guān)的狀態(tài),并且和其他Actor處于物理隔離狀態(tài)。Actor模型內(nèi)部的狀態(tài)由它自己維護(hù)即它內(nèi)部數(shù)據(jù)只能由它自己修改(通過消息傳遞來進(jìn)行狀態(tài)修改),同時(shí)Actor內(nèi)部是以單線程的模式來執(zhí)行的,所以使用Actors模型進(jìn)行并發(fā)編程可以很好地避免數(shù)據(jù)不同步的問題。
在 Swift 當(dāng)中,actor 包含 state、mailbox、executor 三個(gè)重要的組成部分,其中:
- state 就是 actor 當(dāng)中存儲(chǔ)的值,它是受到 actor 保護(hù)的,訪問時(shí)會(huì)有一些限制以避免數(shù)據(jù)競爭(data race)。
- mailbox 字面意思是郵箱的意思,在這里我們可以理解成一個(gè)消息隊(duì)列。外部對(duì)于 actor 的可變狀態(tài)的訪問需要發(fā)送一個(gè)異步消息到 mailbox 當(dāng)中,actor 的 executor 會(huì)串行地執(zhí)行 mailbox 當(dāng)中的消息以確保 state 是線程安全的。
- executor,actor 的邏輯(包括狀態(tài)修改、訪問等)執(zhí)行所在的執(zhí)行器。
在Swift中定義一個(gè)Actor和定義一個(gè)Class是類似的,只是關(guān)鍵字由class改成了actor。Actor也同樣支持構(gòu)造器,屬性和方法,也支持索引器。actor甚至支持protocol和模版元編程。不過在定義actor的屬性時(shí)需要立即初始化構(gòu)造。
actor BankAccount {
let accountNumber: String
var balance: Double
init(accountNumber: String, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
Actor是一個(gè)引用類型,和struct值類型不同,Actor更像是確保了數(shù)據(jù)線程安全的 class,例如:
let account = BankAccount(accountNumber: 1234, initialDeposit: 1000)
let account2 = account
print(account === account2) // true
我們可以用類似于 class 的方式來構(gòu)造 actor,并且創(chuàng)建多個(gè)變量指向同一個(gè)實(shí)例,以及使用 === 來判斷是否指向同一個(gè)實(shí)例。程序運(yùn)行時(shí),我們也可以看到 account 和 account2 指向的地址是相同的:

同時(shí)actor不支持繼承。

actor如何解決數(shù)據(jù)競爭問題?
以典型的銀行賬號(hào)系統(tǒng)為例,在以往的編程中,我們可以使用串行隊(duì)列將所有的異步線程調(diào)用都在串行的隊(duì)列中進(jìn)行操作。
final class BankAccountWithQueue {
let accountNumber = "XXXXXXX"
/// A combination of a private backing property and a computed property allows for synchronized access.
private var _balance: Double = 0
var balance: Double {
queue.sync {
_balance
}
}
/// A concurrent queue to allow multiple reads at once.
private var queue = DispatchQueue(label: "bank.deposit.queue", attributes: .concurrent)
func deposit(amount: Double){
/// Using a barrier to stop reads while writing
queue.sync(flags: .barrier) {
_balance += amount
}
}
func withdraw(amount: Double) {
/// Using a barrier to stop reads while writing
queue.sync(flags: .barrier) {
_balance -= amount
}
}
}
在actor中,不能直接修改通過修改屬性方式來操作balance。Actor為了實(shí)現(xiàn)屬性隔離,actor 的可變狀態(tài)只能在 actor 內(nèi)部被修改,同時(shí)要求對(duì)actor的狀態(tài)修改都通過郵件方式,actor在收到郵件后會(huì)一一進(jìn)行處理并異步返回結(jié)果(有點(diǎn)像我們上面的queue的實(shí)現(xiàn))。
針對(duì)BankAccout如果要進(jìn)行存錢,函數(shù)實(shí)現(xiàn)如下:
extension BankAccount {
func deposit(amount: Double) async {
assert(amount >= 0)
balance = balance + amount
}
}
現(xiàn)在我們可以通過代碼來操作錢包賬戶了
let account = BankAccount(accountNumber: 1234, initialDeposit: 1000)
print(account.accountNumber) // OK,不可變狀態(tài)
print(await account.balance) // 可變狀態(tài)的訪問需要使用 await
await account.deposit(amount: 90) // actor 的函數(shù)調(diào)用需要 await
print(await account.balance)
上面的代碼可以發(fā)現(xiàn)accountNumber的訪問可以直接進(jìn)行,但是balance需要使用await調(diào)用,同樣對(duì)于方法的調(diào)用由于函數(shù)簽名為async,也需要進(jìn)行await。實(shí)際上就是await調(diào)用封裝了發(fā)郵件的過程。
我們再來看一下轉(zhuǎn)賬的實(shí)現(xiàn):
extension BankAccount {
enum BankError: Error {
case insufficientFunds
}
func transfer(amount: Double, to other: BankAccount) async throws {
assert(amount > 0)
if amount > balance {
throw BankError.insufficientFunds
}
balance = balance - amount
// other.balance = other.balance + amount 錯(cuò)誤示例
await other.deposit(amount: amount) // OK
}
}
account可以修改自己的balance但是不能修改other的balance。因?yàn)閠ransfer函數(shù)處理郵件僅限作用于自己的郵件,如果要修改其他實(shí)例對(duì)象的狀態(tài)只能調(diào)用其他實(shí)例對(duì)象的方法??梢钥闯鯽ctor的狀態(tài)要求只能在自己的實(shí)例中修改,不能跨實(shí)例修改狀態(tài)。
Actor的屬性默認(rèn)都是隔離的,但有時(shí)候一些屬性可能不需要進(jìn)行保護(hù),比如BankAccount的accountNumber在構(gòu)造后就不可變。Swift也允許為Actor聲明不需要隔離的屬性:
actor BankAccount {
nonisolated let accountNumber: String
}
同時(shí)也可以用nonisolated修飾函數(shù)。但是nonisolated修飾的函數(shù)不能直接訪問被隔離的狀態(tài),只能像外部函數(shù)一樣使用await來異步訪問。
extension BankAccount : CustomStringConvertible {
nonisolated var description: String {
"Bank account #\(accountNumber)"
}
nonisolated func desc() async{
print(self.accountNumber)
print( await self.balance)
}
}
@MainActor和main queue
有了Concurrency中的Actor概念,SwiftUI 中也引入了@MainActor的裝飾器,使用@MainActor裝飾器可以讓一個(gè)類或者函數(shù)都在主線程執(zhí)行,使用MainActor.run()還可以將一些任務(wù)推送到主線程執(zhí)行。
async {
await MainActor.run {
// Perform UI updates
}
}
這在開發(fā)UI關(guān)聯(lián)狀態(tài)Model的時(shí)候非常有用,在Combine中我們常常定義一個(gè)實(shí)現(xiàn)ObservableObject類對(duì)象,并用@Published來修飾可能會(huì)發(fā)生變化的狀態(tài)屬性,通過@MainActor裝飾器可以保障我們的UI更新都是在主線程進(jìn)行
@MainActor
class AccountViewModel: ObservableObject {
@Published var username = "Anonymous"
@Published var isAuthenticated = false
}
在SwitUI中Apple更進(jìn)一步,對(duì)使用@StateObject和@ObservedObject, Swift會(huì)確保其對(duì)UI的更新運(yùn)行在Main Actor之上,這樣你有時(shí)候在開發(fā)SwiftUI程序時(shí)不小心在異步線程更新了狀態(tài),SwitUI的body方法仍然會(huì)在主線程進(jìn)行更新。
struct ContentView: View {
@StateObject private var accountViewModel = AccountViewModel()
}
雖然SwiftUI會(huì)對(duì)@StateObject和@ObservedObject對(duì)象在body的方法更新上保障在主線程執(zhí)行,仍然還是建議對(duì)UI所監(jiān)聽的對(duì)象添加@MainActor裝飾器,這樣可以保證所有對(duì)UI的修改能在主線程執(zhí)行(不能保證其他非body方法沒有對(duì)UI對(duì)象進(jìn)行訪問和修改),尤其針對(duì)一些從服務(wù)器返回?cái)?shù)據(jù)的異步方法調(diào)用很有效果。