フューチャー技術ブログ

Swift6移行に向けて、Isolation domainとSendableを理解する

本記事は、Swift Advent Calendar 2024の8日目です。
7日目は、@hinakkoさんのSplit Viewを考慮したSize Classを使用したiPad対応です。

HealthCare Innovation Group(HIG)1の橋本です。

先日参加したSwiftZoomin#20の内容から、Swift6移行に向けて理解が必要なSwift Concurrencyの重要な概念について簡単にまとめました。

SwiftZoomin#20の動画は、次のリンク先からYoutube上で視聴可能です。

Swift6移行に向けて、重要な概念3つ

以下3つの概念についてまとめいきます。

  • Isolation domain
  • Isolation boundary
  • Sendable

Isolation domain

隔離=Isolationする領域のことをIsolation domainと呼びます。
Isolation domainの重要な特性は、一つのIsolation domainのなかでは同時に一つの処理しか実行されないことです。(=一つのIsolation domainの中で並行実行されることはないことと同義)

参考

actor

actorというのは、一つの領域で隔離(Isolation)することでデータ競合を防いでいます。

最も馴染み深いのは、MainActorIsolation domainであり、actorのインスタンスに紐づいたIsolation domainも存在します。

MainActorIsolation domainMain Actorで保護された領域を表します。
Actorの場合は、ActorのインスタンスごとにIsolation domainをもち、同じactorでもインスタンスが異なれば、それぞれ異なるIsolation domainを持ちます。

actor Counter {...}

let a = Counter() // aのIsolation domain、aとbは別のIsolation domainである。
let b = Counter() // bのIsolation domain、aとbは別のIsolation domainである。

参考

Isolation boundary

Isolation domainが複数存在しているときのそれぞれのIsolation domainの間の境界のことをIsolation boundaryと呼びます。

アプリにおいては、それぞれのIsolation domain間でIsolation boundaryを超えて情報のやり取りをしないといけない場面が多々あります。

このIsolation boundaryを超えて値を受け渡すために、次のSendableが必要になってきます。

Sendable

Sendable 並行にアクセスされても安全な型だけが準拠できるプロトコル
Sendableの特徴は、Sendableに準拠した型の値だけがIsolation boudnaryを超えられるようになります。
つまり、Sendableを使うことで実行時にデータ競合が起こらないことをコンパイル時にチェックできるように、型の問題とすることで実現しています。

non-Sendable

Sendableに対して、non-Sendableな値は、Isolation boundaryを超えることができません。

Sendableを体感する

上記のBoxクラスをSendableに準拠させて、並行にアクセス可能にさせてみる。

final class Box: Sendable {
var value: Int = 0 // ここでエラー(Stored property 'value' of 'Sendable'-conforming class 'Box' is mutable)
}

そうすると、次のようにSendableに準拠すること自体がエラーであることがわかります。
Box型のプロパティValueが可変状態のため、並行にアクセスすると危険である旨のメッセージが出てきます。

Stored property 'value' of 'Sendable'-conforming class 'Box' is mutable

ここで、varではなく、letだったらどうなるか考えてみます。

final class Box: Sendable {
let value: Int = 0
}

letにすることで、Boxクラスのエラーは解消されます。

valueプロパティが定数になるため、run()メソッド内でbox.balue = -1で書き込みをしていた箇所がコンパイルエラーになります。

この部分を読み込むだけのprint(box.value)とすれば、データ競合を起こさない安全なコードとすることができます。データ競合は、並行にアクセスする少なくとも1つで書き換えが起こっているときに生じるものであるため、今回のように読み込むだけであれば、データ競合の危険性はありません。

func run() {
let box: Box = .init()

Task {
// box.value = -1 // Cannot assign to property: 'value' is a 'let' constant
print(box.value)
}

Task {
print(box.value)
}
}

Isolation boundaryを体感する

Sendableに準拠していないnon-SendableBoxクラスがIsolation boundaryを超えるとコンパイルエラーが出る例を示します。

final class Box {
var value: Int = 0
}


actor A {
var box: Box?
func setBox(_ box: Box) {
self.box = box
}
}


func run() async {
let box: Box = .init()
let a: A = .init() // actor A のインスタンスa

await a.setBox(box) // actor A のインスタンスaにboxを渡す ⇐ boxがIsolation bounadaryを超えて、actor AのインスタンスaのIsolation domainに入る。
// ここで次のエラーメッセージが出る。
// Sending 'box' risks causing data races

print(box.value)
}

Sendableに準拠していないBoxクラスのboxインスタンスがIsolaton boudaryを超えたため、このエラーがでてきます。

このように、Isolaton boudaryが存在していることを体験することができました。

おわりに

SwiftZoomin#20(感覚的に理解するConcurrency: Swift 6はIsolationとSendableを用いてどのようにデータ競合を防止するか)動画前半の内容について私なりにまとめさせていただきました。(@koherさん、大変わかりやすく説明いただきありがとうございました。)

Swift6への移行は、コンパイル時にデータ競合が起こる可能性のある箇所を事前につぶすことができるので、アプリの品質向上に直結するため、ビジネス的にもとても有用なものだと思っております。

本記事がSwift6移行に向けて、少しでも参考になれば幸いです。


  1. 1.医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。