2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ActorにMainActor.runのようなメソッドを追加してsetter処理を行う

Last updated at Posted at 2025-04-15

背景

Swift Concurrencyでは、Actorのプロパティの読み取り(getter)はawaitで非同期で呼び出せますが、書き込み(setter)はそのActorコンテキスト内でしか行えません1

例えば、残高をもつBankAccountというActorがあるとします。

actor BankAccount {
  var balance: Double
}

このActorのプロパティbalanceは、Actorコンテキスト内でのみ更新できます。
そのため、外部からbalanceを更新したい場合はインスタンスメソッドを定義して、その中でプロパティを更新する必要があります。

extension BankAccount {
  // プロパティのsetterはActorコンテキスト外からは呼び出せないため、
  // Setterメソッドを定義する必要がある
  func setBalance(_ newBalance: Double) {
    balance = newBalance
  }
}

Task.detached {
  // ✅: getは可能
  print(await account.balance)

  // ❌: cross-actor property mutations are not permitted. setは許可されていない
  await account.balance = 1000.0

  // ✅: setはActorメソッド経由で呼び出す
  await account.setBalance(1000.0)
}

このように、プロパティをActorコンテキスト外で更新するためにはSetterメソッドを定義する必要があります。
これは少し煩わしさを感じますし、コード量も増えてしまいます。

回避策 なMainActor.runをActorにも実装する

一方で、SwiftのMainActorには、次のような便利なメソッドが用意されています。

await MainActor.run {
    mainActorClass.value = 1
}

このrunメソッド内では、MainActorにisolateされたプロパティの値を自由に更新できます。
この仕組みを一般のActorでも使えるようにすれば、いちいちSetterメソッドを書く必要がなくなるのでは?というのが今回の主旨です。

MainActor.runの実装はこちらにある通りですが、MainActorはGlobal Actorなのに対し、通常のActorはインスタンスごとにisolationがあるため、インスタンスメソッドとして実装する必要があります。

以下のように拡張すれば、任意のActorに対して同様のことができるようになります。

extension Actor {
  func run<T>(body: @Sendable (isolated Self) throws -> T) rethrows -> T {
    return try body(self)
  }
}

このrunメソッドを使えば、次のようにプロパティの書き換えがシンプルに書けます。

Task.detached {
  // ✅: getはそのままでOK
  print(await account.balance) 

  // ✅: setはrunメソッドを通して呼び出せる
  await account.run { $0.balance = 1000.0 }
}

getterとsetterの間にサスペンションポイントは入らないため、この方法でRace Conditionが発生することはありません。

注意点

この方法は便利ですが、Actorの設計思想とはややズレている可能性があります。
Apple Forumでも指摘されているように、Actorのプロパティは「内部状態」を表すものであり、外部から直接頻繁に書き換えるような使い方はそもそも想定されていない。という意見もあります。

もし外部から頻繁にプロパティを書き換える必要があるような設計であれば、ActorではなくMutex(ロック)を使った排他制御の方が適しているかもしれません。
なるべく設計思想に沿った使い方を心がけるべきでしょう。

私自身は、このrunメソッドはプロダクションコードでは使っておらず、ユニットテストのときにActorの内部状態を任意に設定したいケースで活用しています。

参考文献

  1. Actorのプロポーザルではその理由を以下のように説明しています。
    Proposal

    Cross-actor references to an actor property are permitted as an asynchronous call so long as they are read-only accesses:

    Rationale: it is possible to support cross-actor property sets. However, cross-actor inout operations cannot be reasonably supported because there would be an implicit suspension point between the "get" and the "set" that could introduce what would effectively be race conditions. Moreover, setting properties asynchronously may make it easier to break invariants unintentionally if, e.g., two properties need to be updated at once to maintain an invariant.

    (翻訳) クロスアクター間でプロパティの代入をサポートすることは可能です。しかし、inout 操作をクロスアクターでサポートすることは現実的ではありません。なぜなら、get と set の間に暗黙的なサスペンションポイント(中断点)が入り得るため、事実上の競合状態(レースコンディション)を引き起こす可能性があるからです。さらに、非同期にプロパティを設定することで、たとえば2つのプロパティを同時に更新して初めて成り立つ不変条件(インバリアント)を意図せず破ってしまいやすくなる可能性があります。

2
1
0

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?