【Swift】actor境界侵犯エラーの分かりやすいパターンと分かりにくいパターンを見ていく
オブジェクトがSendableならactor boundaryを超えられます。
In some cases, all values of a particular type are safe to pass across isolation boundaries because thread-safety is a property of the type itself. This is represented by the Sendable protocol.
(引用: https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Sendable-Types)
ただし、はっきりそうとわかるパターンと、「あれ、これSendable必要だったの?」と分かりづらいパターンがあり、後者の場合よくSending main actor-isolated 'xx' to nonisolated instance method 'yyy' risks causing data races between nonisolated and main actor-isolated uses
という境界侵犯エラーを踏んでしまうので、理解して物事を進めていけるようにそれぞれのパターンを見ていきます。
はっきりSendableが必要だとわかるパターン
あるアクター(MyActor1)から、同一の分離ドメインでないアクターに対して、パラメーターでSendableでないオブジェクトを渡そうとしています。
@MainActor
class MyActor1 {
var value = ValueObject()
func onButtonTap() async {
// 🚨 Sending 'self.value' risks causing data races
try? await MyActor2().execute(value)
}
}
// Sendableではない
class ValueObject {
var value: Int = 0
}
actor MyActor2 {
func execute(_ value: ValueObject) async throws {}
}
上記の問題を解決するにはシンプルで、ValueObject
をSendableにするだけです。この場合はstruct
にしてしまうのが最も簡単です。
struct ValueObject {
var value: Int = 0
}
おそらくパラメーターでの受け渡しのイメージが、WWDCセッションでの島を用いたメンタルモデルに近いので、割とすんなり分かった気になれるんだと思います。
Sendableが必要であることがわかりにくいパターン
SendableでないModelObject
をMainActorのMyActor1
でプロパティとして保持しており、そのメソッドのmodel.execute()
を呼び出そうとしています。
@MainActor
class MyActor1 {
let model = ModelObject()
func onButtonTap() async {
// 🚨Sending 'self.model' risks causing data races
try? await model.execute()
}
}
class ModelObject {
func execute() async throws {}
}
execute() async throws
はメソッドパラメーターでSendableではない値の受け渡しをしているわけではありません。ではなぜエラーになるのでしょう?
Swiftの言語仕様に深く踏み入った理解は提案できませんが “Sending risks data races” error, but everything is on the Main Actorのトップコメントで、大変理解を容易にするコメントがあったので引用します。
その型のメソッドを呼び出す際に self が暗黙的に渡されており、その self が別のアイソレーション(隔離領域)に渡されるようなもの。
(You can think of this as call to the method on this type implicitly passes self there, which is then passed to different isolation.)
つまり、上記の考え方を借りると、メソッド呼び出し時にfunc execute(self: ModelObject) async throws
という暗黙のselfパラメーターがあって、利用側がtry? await model.execute(self: model)
と利用しているように考えると、島のメンタルモデルでこの問題を捉えられるようになります。この場合、「分離境界で非SendableのModelObject
を受け渡そうとしているので問題がある」ということが大変理解しやすいのではないでしょうか。
上記の例でも、ModelObject
をSendableにすることで問題は解決します。struct
にしても良いですが、何らmutable stateを持っていないので、単にSendable
に適合するだけでも問題ありません。
まとめ
ぱっと見でSendableが必要か分かりやすいケースと、分かりにくいケースを見ていきました。分かりにくいケースでは「暗黙的にメソッドでselfを渡しているようなもの」と考えることで、WWDCでよく見かける島のメンタルモデルに落とし込んで理解を容易にすることができました。
Swift言語にディープな理解なお持ちの方がいらっしゃれば、「分かりにくいパターン」としてあげた問題について、ぜひ解説コメント頂けると助かります。
Discussion