Actor型とは何か
WWDC2021の0306-actors.mdをまとめてみました。
自分なりの解釈を含むので間違っていたら指摘していただけると助かります。
Actor型とはSendableプロトコル、非同期関数(async)を用いてデータの排他性を維持する型です。
Actor型は並列ステータスを静的に監視し、同時アクセスしても単一スレッドのみが、特定の時間にそのデータにアクセスすることを保証します。なので、データ競合(=data races。データの書き換えが頻繁に起こり管理できない状態)の発生を容易に回避できるようになります。
Actor型
アクター分離(Actor isolation)
Actor型はメンバ変数や関数がアクター分離されます。
隔離されたデータは自身と同じ型であってもインスタンスが異なる場合、通常呼び出し(同期処理)では排他性が維持できないのでアクセスできません。
クロスアクター参照(Cross-actor reference)
アクター分離されたメンバ変数や関数にアクセスすることをクロスアクター参照といいます。
メンバ変数や関数にアクセスできるのは、同じActor型の同じインスタンス内からのアクセスか、非同期関数(async)、非同期アクセスのプロパティ読み込みになります。また、Actor型のメンバ変数は関数の引数や戻り値は、排他制御を保持する代償に型(下記、クロスアクター参照とSendableプロトコル参照)の制約を受けます。
// Actor型
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
extension BankAccount {
enum BankError: Error {
case insufficientFunds
}
// 自分以外の銀行口座へ送金する。
func transfer(amount: Double, to other: BankAccount) throws {
if amount > balance {
throw BankError.insufficientFunds
}
print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
// 残高を減らす。
// 自分自身のインスタンスはActor型でもアクセスできる。
balance = balance - amount
// 他の銀行口座をその分増やす。
// クロスアクター参照(Cross-actor reference)
// Actor型から自分のインスタンス以外のActor型の変数や関数にアクセスすると、
// コンパイルエラーになる。
other.balance = other.balance + amount // error: actor-isolated property 'balance' can only be referenced on 'self'
}
Actor型への非同期関数(async)の参照
非同期関数(async)でアクセスした場合、暗黙的に、アクセスがメッセージ(呼び出し要求)に変換され、Actorの排他制御が確保されたタイミングで処理されます。
extension BankAccount {
// 自分以外の銀行口座へ送金する。
// 自身のインスタンス以外の銀行口座(Actor型)へアクセスするため、
// 非同期関数(async)にする。
func transfer(amount: Double, to other: BankAccount) async throws {
if amount > balance {
throw BankError.insufficientFunds
}
// 残高を減らす。
// 自分自身はActor型だが、自身のインスタンスなので問題なし。
balance = balance - amount
// 他の銀行口座をその分増やす。
// otherは非同期関数(async)でアクセスする必要がある。
await other.deposit(amount: amount)
}
func deposit(amount: Double) async {
balance = balance + amount
}
}
transfer関数の時点で非同期アクセスする必要があるので、既に排他的な同期済みのアクター内では、deposit関数を非同期で呼び出す必要なはい。以下の状態でも問題はない。
extension BankAccount {
// 自分以外の銀行口座へ送金する。
// 自身のインスタンス以外の銀行口座(Actor型)へアクセスするため、
// 非同期関数(async)にする。
func transfer(amount: Double, to other: BankAccount) async throws {
if amount > balance {
throw BankError.insufficientFunds
}
// 残高を減らす。
// 自分自身はActor型だが、自身のインスタンスなので問題なし。
balance = balance - amount
// 他の銀行口座をその分増やす。
// otherは既に非同期関数(async)のtransfer関数で排他制御されており、
// depositは非同期関数(async)でなくても良い。
other.deposit(amount: amount)
}
func deposit(amount: Double) {
balance = balance + amount
}
}
クロスアクタープロパティ(Cross-actor property sets)
アクターのプロパティへのクロスアクター参照は、読み取り専用のアクセスである限り、非同期呼び出しとして許可されます。
func checkBalance(account: BankAccount) async {
// awaitで非同期参照できる。
print(await account.balance) // okay
// 書き込みは許可されない。
await account.balance = 1000.0 // error: cross-actor property mutations are not permitted
func printAccount(account: BankAccount) {
// let型は非同期で参照する必要がないが、異なるインスタンスのActorの場合は、
// awaitで非同期参照する必要がある。
print("Account #\(await account.accountNumber)")
}
クロスアクター参照とSendableプロトコル
Sendableプロトコルとは
Actor型は排他制御可能な型として、Swift.5.5から用意されるSendableプロトコルに準拠させる必要があります。Sendableプロトコルに準拠できる型は、同時書き換えに対応できるように値をコピーしてデータの整合性を保持する対処が施されます。
Sendableプロトコルの適合条件
Sendableプロトコルは、構造体かEnunに適用(プロトコルを継承)するとができ、全てのメンバが、Int、Double、String、Dateなど値を意味する型、[String]や[Int: String]など、コレクションを意味する型、メタタイプか、中身が全て適合したタプルか、中身が全て適合した構造体か、中身が全て適合したEnum型である必要があります。
クロスアクター参照可能な型
クロスアクター参照時の型は、プロパティ参照や関数の引数、戻り値、がSendableプロトコルに適用できる型か、Sendableプロトコルを準拠している必要があります。
// メンバはクロスアクタ参照可能な型だが、クロスアクター参照でクラス型はサポートしていない。
// 使用するには、structにしてSendableプロトコルを適用する必要がある。この例ではしない。
/*
struct Person: Sendable {
var name: String
let birthDate: Date
}
*/
class Person {
var name: String
let birthDate: Date
}
// Actor型はSendableプロトコルに準拠する必要があるので
// var owners: [Person]のチェックを無効にする
// @unchecked が必要なのではと推測する。
actor BankAccount {
var owners: [Person]
func primaryOwner() -> Person? { return owners.first }
}
// 非同期アクセスに対応しない型(Person)を返すのでコンパイルエラー
if let primary = await account.primaryOwner() { // error: cannot call function returning non-Sendable type 'Person?' across actors
primary.name = "The Honorable " + primary.name
}
// メンバはクロスアクタ参照可能な型だが、クロスアクター参照でクラス型はサポートしていない。
// 使用するには、structにしてSendableプロトコルを適用する必要がある。この例ではしない。
class Person {
var name: String
let birthDate: Date
}
// @unchecked
actor BankAccount {
var owners: [Person]
func primaryOwner() -> Person? { return owners.first }
func primaryOwnerName() -> String? {
// Personは排他制御に対応できないクラス型だが、
// 同じActor型のインスタンス内でアクセスする分には、
// 排他性が維持されるので問題ない。
return primaryOwner()?.name
}
}
// String?は非同期アクセスに対応する型なので問題なし。
let primaryName = await account.primaryOwnerName(
クロスアクター参照とクロージャー
Actor型内で定義したクロージャーのアクター隔離はSendable(排他制御可能な型を使用するクロージャー)で定義した場合とそうでない場合で異なります。
クロージャーが@Sendableで定義されていない場合、Actor型がクロージャーのアクター隔離を行います。
クロージャーが@Sendableで定義された場合、アクター隔離はクロージャ自身が行うため、Actor型がアクター隔離を行うことはありません。
クロージャーのアクター隔離
排他的なクロージャの呼び出しと推測します。
extension BankAccount {
// 月末レポートを作成するタスクをスケジュールする。
func endOfMonth(month: Int, year: Int) {
// detachクロージャーは@Sendableで定義されているため、クロージャー自身が、
// アクター隔離(排他的なクロージャの呼び出し)を行う。
detach {
let transactions = await self.transactions(month: month, year: year)
let report = Report(accountNumber: self.accountNumber, transactions: transactions)
await report.email(to: self.accountOwnerEmailAddress)
}
}
// 口座を閉鎖して別の口座に分配する。
// 別の講座を編集するため非同期関数(async)である必要がある。
func close(distributingTo accounts: [BankAccount]) async {
let transferAmount = balance / accounts.count
// forEachクロージャーは@Sendableで定義されていないため、
// BankAccountアクターが、アクター隔離(排他的なクロージャの呼び出し)を行う。
accounts.forEach { account in
balance = balance - transferAmount
await account.deposit(amount: transferAmount)
}
await thief.deposit(amount: balance)
}
}
アクターの再入可能性(Actor reentrancy)
アクター内の関数はアクター隔離されていますが再入可能です。それによりアクター内の非同期関数(async)がサスペンド(=await、処理待ち)して、再開する前に、他の作業をアクタ上で実行することができます。これをインターリーブ(interleaving)と呼びます。これにより排他処理による処理待ちの機会がなくなりパフォーマンスが向上することができるそうです。
ダウンロードとデコードしてイメージに変換する処理は、ダウンロードとデコードなど部分的なタスクのみシリアル化して、画像のダウンロードとデーコードを同時に処理と良いそうです。
actor ImageDownloader {
var cache: [URL: Image] = [:]
func getImage(_ url: URL) async -> Image {
if let cachedImage = cache[url] {
return cachedImage
}
let data = await download(url)
let image = await Image(decoding: data)
return cache[url, default: image]
}
}
プロトコルの適合性(Protocol conformances)
Actorプロトコル
全てのActor型は新しいプロトコルであるActorに暗黙的に準拠します。
protocol Actor : AnyObject, Sendable { }
拡張された関数は自身のActorがアクター隔離を行います。
protocol DataProcessible: Actor { // このプロトコルに準拠できるのはアクター型のみ。
var data: Data { get } // 自身のActorが隔離する。
}
extension DataProcessible {
func compressData() -> Data { // 自身のActorが隔離する。
// use data synchronously
}
}
actor MyProcessor : DataProcessible {
var data: Data // 自身のActorが隔離する。
func doSomething() {
let newData = compressData() // 自身のActorが隔離する。
}
}
非同期関数(async)のみのプロトコルに限り、Actor型に準拠させることが可能です。
// 非同期関数(async)のみのプロトコル
protocol Server {
func send<Message: MessageType>(message: Message) async throws -> Message.Reply
}
actor MyActor: Server { // プロトコルとして準拠
// 自身のActorが隔離する。
func send<Message: MessageType>(message: Message) async throws -> Message.Reply {
}
}