34
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Swift5.5から使用可能になるActor型について要点を抜粋してみた

Last updated at Posted at 2021-06-19

Actor型とは何か

WWDC2021の0306-actors.mdをまとめてみました。
自分なりの解釈を含むので間違っていたら指摘していただけると助かります。
Actor型とはSendableプロトコル、非同期関数(async)を用いてデータの排他性を維持する型です。

Actor型は並列ステータスを静的に監視し、同時アクセスしても単一スレッドのみが、特定の時間にそのデータにアクセスすることを保証します。なので、データ競合(=data races。データの書き換えが頻繁に起こり管理できない状態)の発生を容易に回避できるようになります。

Actor型

アクター分離(Actor isolation)

Actor型はメンバ変数や関数がアクター分離されます。
隔離されたデータは自身と同じ型であってもインスタンスが異なる場合、通常呼び出し(同期処理)では排他性が維持できないのでアクセスできません。

クロスアクター参照(Cross-actor reference)

アクター分離されたメンバ変数や関数にアクセスすることをクロスアクター参照といいます。
メンバ変数や関数にアクセスできるのは、同じActor型の同じインスタンス内からのアクセスか、非同期関数(async)、非同期アクセスのプロパティ読み込みになります。また、Actor型のメンバ変数は関数の引数や戻り値は、排他制御を保持する代償に型(下記、クロスアクター参照とSendableプロトコル参照)の制約を受けます。

Actor型の定義のコンパイルエラー
// 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の排他制御が確保されたタイミングで処理されます。

非同期関数(async)であれば問題なし
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関数を非同期で呼び出す必要なはい。以下の状態でも問題はない。

さらにasyncを取る
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がアクター隔離を行います。

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 {
  }
}
34
27
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?