Sendableじゃない型がactor boundaryを超える!?Swift 6からのRegion Based Isolaton
Swift 6からの新しい挙動として、Region Based Isolationというものがあります。
これはSendableじゃない型でもactor boundaryを超えることができるという変わった挙動をします。
今までだとどうなっていたのか
Swift 5環境でStrict Concurrencyを有効化すると以下のコードは警告が出ます。
どこで警告が出そうか注意深く見てみてください。
func updateView(view: ComplexLayoutView) async {
// どのactorにも隔離されていない関数
let layout = ComplexLayout.calculate()
await view.apply(layout)
}
// Non Sendable
class ComplexLayout {
// ...(レイアウト情報のプロパティ)
static func calculate() -> ComplexLayout {
//...(レイアウトの計算)
return ComplexLayout()
}
}
@MainActor
class ComplexLayoutView {
func apply(_ layout: ComplexLayout) {
// 引数の情報をもとにレイアウトを更新
}
}
答えはここです。
func updateView(view: ComplexLayoutView) async {
// どのactorにも隔離されていない関数
let layout = ComplexLayout.calculate()
await view.apply(layout) // ⚠️ Passing argument of non-sendable type ...
}
警告の内容は次の通りです。
Passing argument of non-sendable type 'ComplexLayout' into main actor-isolated context may introduce data races
つまり、どのactorにも隔離されていないupdateView
の中で作ったnon-sendableなlayout
が、MainActorに隔離されているComplextLayoutView
にも共有されており、data raceを引き起こす可能性があるというものです。
しかし、これは本当にdata raceを引き起こすのでしょうか?
data raceは複数のスレッドから同時にアクセスすることで発生します。
しかし上のupdateView
の中のコードでlayout
は作られた後にapply
関数に渡してから、updateView
の中では一度も触られていません。
つまり、このlayout
の場合、MainActorに渡した後は他のどのactorからも触れることはなく、data raceを引き起こしません。
こういったケースのために導入されたのがRegion Based Isolationです。
Swift 6からはどうなるのか
上のコードは、警告がでずにビルドできます。
しかし、あくまでこれは同時にアクセスされないことが保証できる場合のみケースです。
次のようなコードはエラー(or 警告)がでます。
class ComplexLayout {
// ..
func printInformation() { ... }
}
func updateView(view: ComplexLayoutView) async {
let layout = ComplexLayout.calculate()
await view.apply(layout) // ⚠️ Passing argument of non-sendable type ...
layout.printInformation()
}
この場合MainActorにlayout
を渡した後、updateView
の中で再度layout
を触っているためです。
これは複数のスレッドから同時にアクセスされる可能性が残るため、コンパイラがそれを指摘します。
sending
キーワード
sending
キーワード
引数に対する上の例の場合、layout
を関数の中で作っていました。
これが引数から渡される場合、呼び出し元の関数で引き続きlayout
を触る可能性があるため、警告が出ます。
func updateView(view: ComplexLayoutView, with layout: ComplexLayout) async {
await view.apply(layout) // ⚠️ Passing argument of non-sendable type ...
}
こういった場合、sending
キーワードを引数に足すことで回避できます。
このsending
は別のactorにパスされることを示したものです。
以下のコードは何も警告が出ません。
func updateView(view: ComplexLayoutView, with layout: sending ComplexLayout) async {
await view.apply(layout)
}
func anotherFunction() async {
let view = ComplexLayoutView()
let layout = ComplexLayout.calculate()
await updateView(view: view, layout: layout)
}
しかし、updateView
の後にlayout
にアクセスすると警告が出ます。
これは、updateView
の中で別のactorにlayout
を渡した後に引き続きlayout
にアクセスしているからです。
func anotherFunction() async {
let view = ComplexLayoutView()
let layout = ComplexLayout.calculate()
await updateView(view: view, layout: layout) // ⚠️ Sending 'layout' risks causing data races...
layout.printInformation()
}
sending
キーワード
返り値に対するこのsending
キーワードは関数の返り値の型にもつけることができます。
関数の返り値についていた場合、その返り値がどこからもアクセスされないことを意味します。
以下のcalculate
関数は問題ありませんが、calculateWithCache
関数は警告になります。
なぜなら、返り値のComplexLayout
は別のactorにパスされる可能性がありますが、それをcacheとしてキャプチャーしていて、他のスレッドから呼び出される可能性が残っているからです。
class ComplexLayoutLayout {
static func calculate() -> sending ComplexLayout {
ComplexLayout()
}
@FooActor private static var cache: ComplexLayout?
@FooActor static func calculateWithCache() -> sending ComplexLayout {
if let cache {
return cache
}
let layout = ComplexLayout()
cache = layout
return layout
} // ⚠️ Sending 'layout' risks causing data races...
}
Xcode 16 Beta 2での状況
残念ながら、この記事執筆時の最新であるXcode 16 Beta 2では、Task
に対してはこの機能が動かないようです。
そのため、以下のようなコードは警告が出てしまいます。
let layout = ComplexLayout.calculate()
Task { @MainActor in
view.apply(layout)
}
ただ、どうやら少し前に、Taskに対してもこれが正しく動くようにするPRがマージされたらしいので、次のBetaやXcode 16の正式リリースには含まれることが期待できます。
おわりに
Swift 6で有効化されるRegion Based Isolationというものを紹介しました。
これがどういう仕組みで動いてるのか、詳しい挙動についてはSE-414をご確認ください。
ただし複雑な内容な上、ページの中でも「開発者がその細かい挙動までを理解する必要はない」と述べられているため、無理に追う必要はないと思います。
Discussion