はじめに
Swift 6 言語モードでは、コンパイラが並行プログラミングにおけるデータ競合を検出し、安全性を保証できるようになりました。それに伴い、Concurrencyへの対応が開発において必須となってきています。本記事では、クイズ形式でSwift Concurrencyの基本を楽しく学べるようにまとめています。ぜひ挑戦してみてください!
問題1 データ競合とActorの役割
下記のコードを実行した場合、コンパイルは成功するでしょうか?
また、コンパイルエラーが発生する場合、その理由を説明し、コードを修正してデータ競合を解消してください。
※全ての問題はSwift 6 言語モードで実行されます
final class Counter {
var value: Int = 0
}
func run() {
let counter: Counter = .init()
Task {
counter.value = 1
}
Task {
print(counter.value)
}
}
run()
解答
コンパイルは成功しません。Counter
クラスのプロパティ value に対して、2つの異なる Task から並行してアクセスしデータ競合の可能性があるため、コンパイルエラー発生します。Swift 6 では、並行処理で使用される型は Sendable プロトコルに準拠している必要があります。Counter
は Sendable を満たしていないため、このコードはコンパイルできません。
actor を使用して、データ競合を防ぎ、スレッドセーフな設計に修正します。
actor Counter {
var value: Int = 0
}
func run() {
let counter = Counter()
Task {
await counter.value = 1
}
Task {
print(await counter.value)
}
}
run()
データ競合について
データ競合(Data Race)とは、複数のスレッドまたはタスクが、同じメモリ位置に同時にアクセスし、少なくとも1つが書き込み操作を行う場合に発生する問題です。データ競合が発生すると、以下のような問題が生じる可能性があります。
予測不能な動作: 同じデータに対する操作順序が保証されないため、意図しない結果になる。
クラッシュのリスク: 不整合なデータ状態により、アプリがクラッシュする可能性がある。
デバッグが困難: データ競合はタイミング依存のため、再現性が低くバグの追跡が困難。
Actorについて
Actor はスレッドセーフな型を提供し、並行処理でのデータ競合を防ぎます。
Actorは内部でシリアルキューを使用しており、複数のタスクから同時にアクセスされても1つずつ実行されるため、安全にプロパティを操作できます。
Sendable プロトコルについて
Swift 6 の並行処理では、Sendable プロトコルは、タスク間で安全に渡すことができる型であることを保証します。問題のCounter
のように参照型(クラス)はその性質上、同じインスタンスへの複数の参照を持つことが可能です。そのため、プロパティが可変(var)である場合、データ競合のリスクが生じ、Sendable プロトコルに適合できません。
問題2 MainActorとIsolation Domain
下記のコードを実行した場合、コンパイルは成功するでしょうか?
また、コンパイルエラーが発生する場合、その理由を説明し、コードを修正してデータ競合を解消してください。
final class Counter {
var value: Int = 0
}
@MainActor
func run() {
let counter: Counter = .init()
Task {
counter.value = 1
}
Task {
print(counter.value)
}
}
run()
解答
コンパイルは成功します。@MainActor
を使用している場合、コード内の値アクセスは isolation domain(隔離された領域)内で行われており、isolation boundary(隔離境界)を跨ぐことがないため、コンパイルは成功します。
コンパイラがデータ競合を防ぐ方法
Swift のコンパイラは、Isolation Domain を用いて可変状態を管理し、データ競合を防ぎます。以下の仕組みで安全性を保証しています
Isolation Domain
Isolation Domain は、特定のアクターまたはスレッド上で安全にコードを実行するための隔離された範囲を指します。この範囲内では、データ競合を防ぐための管理が行われています。
今回の問題では、@MainActor
によって run
関数が MainActor Isolation Domain に属しています。そして、Task ブロックは 親のコンテキスト(Isolation Domain) を引き継ぐ特性を持つため、暗黙的に MainActor Isolation Domain 内で実行されます。
Isolation Boundary
Isolation Boundary は、異なる Isolation Domain 間でデータや操作をやり取りする際に存在する境界を指します。今回の問題では、Task が MainActor Isolation Domain 内で生成されているため、counter.value へのアクセスは同じ Isolation Domain 内で行われます。
Isolation Domain 内の管理
各 Isolation Domain 内では、は同時に一つの処理しか実行されません。同時に複数の処理が実行されることがないため、データ競合が発生しません。
Isolation Boundary の制約
異なる Isolation Domain 間でデータをやり取りする場合(Isolation Boundary を跨ぐ場合)、そのデータは Sendable プロトコルに準拠している必要があります。
Sendable に準拠していないデータは Isolation Boundary を跨ぐことができず、コンパイラがエラーを発生させます。
問題3 Region based isolation
下記コードはClient クラスと ClientStore アクターを使用して、新しい顧客のアカウントを非同期的に追加する例を示しています。下記のコードを実行した場合、コンパイルは成功するでしょうか?
また、コンパイルエラーが発生する場合、その理由を説明し、コードを修正してデータ競合を解消してください。
class Client {
var name: String
var initialBalance: Double
init(name: String, initialBalance: Double) {
self.name = name
self.initialBalance = initialBalance
}
}
actor ClientStore {
var clients: [Client] = []
static let shared = ClientStore()
func addClient(_ c: Client) {
clients.append(c)
}
}
func openNewAccount(name: String, initialBalance: Double) async {
let client = Client(name: name, initialBalance: initialBalance)
await ClientStore.shared.addClient(client)
}
解答
コンパイルは成功します。Client
クラスは Sendable
プロトコルに準拠していませんが、Swift 6 の Region Based Isolation によって、データ競合が発生しない場合はコンパイルエラーが発生しません。
異なる Isolation Domain 間でデータをやり取りする場合(Isolation Boundary を跨ぐ場合)、そのデータは Sendable プロトコルに準拠している必要があります。
// Sendableプロトコルに準拠していないclientがIsolation Boundaryを跨いでいる
await ClientStore.shared.addClient(client)
Region based isolation
Region Based Isolation では、非Sendableな値が Isolation Boundary を跨ぐ場合でも、値が以降のコードで再利用されないことをコンパイラが検出できればエラーが発生しません。
本実装ではClient
にアクセスできるのはactorであるClientStore
のみであることが保証されています。このようにデータ競合が発生しない状態あのでコンパイルエラーは発生しません。
func openNewAccount(name: String, initialBalance: Double) async {
// Clientを生成
let client = Client(name: name, initialBalance: initialBalance)
// 生成したClientをClientStoreに渡す
await ClientStore.shared.addClient(client)
// 上記以降clientを使用せずに関数を抜ける
}
一方、client を再利用しようとすると次のようにエラーになります。
func openNewAccount(name: String, initialBalance: Double) async {
let client = Client(name: name, initialBalance: initialBalance)
await ClientStore.shared.addClient(client)
// 関数内からもclientにアクセスしClientStoreからもアクセスされることからデータ競合が発生する可能性が生じコンパイルエラーが発生します。
+ print(client.name)
}
まとめ
本記事では、Swift 6 言語モードでの Concurrency における基本概念をクイズ形式で解説しました。Swift 6 のコンパイラでは、データ競合を防ぐ仕組みが強化され、特に Sendable プロトコル と Actor モデル を使用した設計が重要になっています。また、Region Based Isolation の導入により、従来はエラーとなっていたパターンも安全性が保証される場合に限り許容されるようになりました。
おわりに
Swift 6 の Concurrency 機能は、データ競合を防ぐための新しい仕組みを提供するだけでなく、開発者がより安全でスケーラブルなコードを書く手助けをします。クイズ形式の解説を通じて、これらの基本概念を実際のプロジェクトで活用できるようになったでしょうか?ぜひ、Swift Concurrency の機能を実際のコードに活かしてみてください!
コードはこちらから確認できます。
参考文献
Swift 6への移行 - Documentation
Swift 6へのアプリの移行
Region based Isolation
感覚的に理解するConcurrency: Swift 6はIsolationとSendableを用いてどのようにデータ競合を防止するか