🔥

Swiftにおけるactorの利用場面

4 min read

注意事項

再度調査したところこのactorの説明は間違っているようです。
ですがなぜ間違っているかがわからないため現在調査中です。

下で示したコードは何回か実行すると、送金前の残高が表示されてしまいます。

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}

Task {
  await accounts[0].transfer(amount: 100, to: accounts[1])
}

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}

前提知識

Swiftのasync/awaitの知識が必要です。
知らない方はまずそちらから確認したほうがわかりやすいです。

actorのいいところ

actorは非同期処理を含むclassをactorに変更すると、より安全にコーディングすることが可能です。

非同期処理を含むclassを作成した場合に、classでは関数や、プロパティの使用の落とし穴にコンパイル時には気づけませんが、actorではコンパイル時に、エラーとなり気づくことが可能になります。

actorclassの(varプロパティに対して)ルールが厳しい版だと考えるのが簡単だと思います。

actorが有用な場面

  • 銀行を想定

  • 条件

    • 送金処理には2分程度程度かかる
    • 人Aの預金残高300円
    • 人Bの預金残高300円
  • 10:00 人Aが人Bに100円送金

  • 10:01 Bの預金残高を確認(送金されたはずなのに、まだ残高が増えていない)

  • 10:02 人Aから人Bへの送金が完了

まずはactorではなく、classで作ってみる

class BankAccount {
  let accountNumber: Int
  var balance: Double

  init(accountNumber: Int, initialDeposit: Double) {
    self.accountNumber = accountNumber
    self.balance = initialDeposit
  }
  
  func transfer(amount: Double, to other: BankAccount) async {
    print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
    
    balance = balance - amount

    await other.deposit(amount: amount)
  }

  func deposit(amount: Double) async {
      self.balance = self.balance + amount
  }
}
// メイン部分
let accounts: [BankAccount] = [
  .init(accountNumber: 1, initialDeposit: 100),
  .init(accountNumber: 2, initialDeposit: 200),
]

Task {
  print(accounts[0].balance)
  print(accounts[1].balance)
}

Task {
  await accounts[0].transfer(amount: 100, to: accounts[1])
}

Task {
  print(accounts[0].balance)
  print(accounts[1].balance)
}
// 実行結果
100.0
200.0
100.0
200.0
Transferring 100.0 from 1 to 2

上の実行結果の問題点

送金中なのに、残高を参照できてしまい、期待通りの残高を取得することができない。

actorで解決

単純にclassをactorに変えるだけだとprint(accounts[0].balance)に問題があると出てコンパイルできません。

testActor.swift:33:3: error: expression is 'async' but is not marked with 'await'
  print(accounts[0].balance)

修正方法

actorではvarのプロパティは、非同期的に参照する必要があります。

なのでvar balance: Doubleを参照しているprint(accounts[0].balance)を非同期で呼び出す必要があります。

今回の場合BankAccountはclassをactorに変えるだけでほかの編集は必要ありません。メインの呼び出し元にawaitを付けるだけです。

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}

Task {
  await accounts[0].transfer(amount: 100, to: accounts[1])
}

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}
100.0
200.0
Transferring 100.0 from 1 to 2
0.0
300.0

結論

最初にあげた例でいうと、以下のように変わります。

classで実装

  • 10:00 人Aが人Bに100円送金
  • 10:01 Bの預金残高を確認(送金されたはずなのに、まだ残高が増えていない)
  • 10:02 人Aから人Bへの送金が完了

actorで実装

  • 10:00 人Aが人Bに100円送金
  • 10:01 Bの預金残高を確認
    • 確認に1分かかる。送金が完了次第確認が可能(var balance: Doubleが解放され次第)
  • 10:02 人Aから人Bへの送金が完了

actorは便利なツールではないですが、非同期処理を安全に書くツールとして有用です。

最終的なコード

actor BankAccount {
  let accountNumber: Int
  var balance: Double

  init(accountNumber: Int, initialDeposit: Double) {
    self.accountNumber = accountNumber
    self.balance = initialDeposit
  }
  
  func transfer(amount: Double, to other: BankAccount) async {
    print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
    
    balance = balance - amount

    await other.deposit(amount: amount)
  }

  func deposit(amount: Double) async {
      self.balance = self.balance + amount
  }
}

let accounts: [BankAccount] = [
  .init(accountNumber: 1, initialDeposit: 100),
  .init(accountNumber: 2, initialDeposit: 200),
]

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}

Task {
  await accounts[0].transfer(amount: 100, to: accounts[1])
}

Task {
  print(await accounts[0].balance)
  print(await accounts[1].balance)
}

間違っているところがあればコメントでこっそり教えてください。

Discussion

ログインするとコメントできます