【Swift Concurrency】Actorとは?データの競合を防ぐ方法と使い方
この記事からわかること
- Swift Concurrencyとは?
- データの競合とは?
- Actorの使い方
- isolated/nonisolatedキーワードの違い
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
公式リファレンス:Concurrency
公式ドキュメント:Concurrency
Swift Concurrencyとは?
Swift Concurrency(同時実効性)とはiOS15(Swift 5.5)から導入された仕組みの1つで非同期プログラミングをより利用しやすくするための機能を提供しています。例えばよく利用されるasyncやawaitキーワード、Task構造体などはSwift Concurrencyから提供されており、非同期処理を実装する際に利用されるcompletionHandler(コールバック関数)の弱みである可読性の低下を解消することができるようになりました。
データの競合
Actorがなんたるかを理解する前にマルチスレッドプログラミングで起こりうる大きな問題である「データの競合」について理解しておきます。
異なるスレッドから単一のデータにアクセスできるようになっているおかげでタイミングの制御が難しくなりデータに不整合が生じる可能性が発生します。
これを防ぐためにはスレッドAからのデータの操作中にスレッドBからの編集を受け付けないようにするといった排他制御を行う必要があります。
データの競合を実際に発生させてみる
例えば以下のようなクラスがあるとします。
class Score {
private var value: Int = 0
func update(score: Int) {
value = score
print("Score:\(value)")
}
}
このクラスのvalueプロパティをさまざまなスレッドから同時に更新処理を実行してみます。期待としては順番はさておきScore:200、Score:300、Score:400となるように思います。
let score = Score()
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
score.update(score: 200)
print("非同期処理1")
}
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
score.update(score: 300)
print("非同期処理2")
}
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
score.update(score: 400)
print("非同期処理3")
}
しかし実行してみると出力は以下のようになりました。今回はたまたま400でしたが全て200になったり、300になったりもします。
Score:400
非同期処理2
Score:400
非同期処理3
Score:400
非同期処理1
これを防ぐための手段としてActorが活用できます。
Actorとは?
ActorもSwift Concurrencyから提供されている機能の1つで、データの排他制御を行うことができるスレッドセーフなオブジェクトです。Actorはクラスや構造体と同じような振る舞いを持つ型で、クラスと同じく参照型のオブジェクトになります。
Swift ConcurrencyのTaskなどを使用することで並行処理が安全に実行できるようになりましたが共有のデータを操作する際にはデータ競合のリスクが発生します。Actorは並行コード間で安全にデータを操作するためのオブジェクトで、インスタンスには1つのタスクのみがデータにアクセスできるようになっています。
Actorはactorキーワードを使用して定義します。先ほど発生していたデータの競合のコードをActorに置き換えてみます。
actor Score {
private var value: Int = 0
func update(score: Int) {
value = score
print("Score:\(value)")
}
}
Actorのメソッドを呼び出す場合はawaitをつける必要があります。
let score = Score()
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 1秒停止
await score.update(score: 200)
print("非同期処理1")
}
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 2秒停止
await score.update(score: 300)
print("非同期処理2")
}
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 3秒停止
await score.update(score: 400)
print("非同期処理3")
}
出力が期待通りに動作するようになりました。
Score:200
非同期処理1
Score:300
非同期処理2
Score:400
非同期処理3
文法はクラスと一緒
先述しましたがActorはクラスと同じく参照型のオブジェクトでした。基本的な文法はクラスと同じで、プロパティやメソッドの定義、イニシャライザの実装、プロトコルの継承なども可能になっています。
actor Score: Identifiable {
private var value: Int = 0
func update(score: Int) {
value = score
print("Score:\(value)")
}
}
ただしActorを継承してサブActorを作るといったことはできないようになっています。
actor Score {}
actor GameScore: Score {} // Actor types do not support inheritance
プロパティやメソッドの呼び出しは非同期
Actorに定義したプロパティやメソッドを外部から呼び出す場合は自動的に非同期になります。そのためawaitキーワードを使用する必要があり、外部からの参照では別タスクが呼び出し中であればそのタスクの完了を待ってから実行されることになります。
print(await score.value)
await score.update(score: 200)
ただし内部から呼び出す場合は非同期を意識することなく普通に参照することが可能になっています。
actor Score {
private var value: Int = 0
func update(score: Int) {
value = score
print("Score:\(value)")
}
}
nonisolated
Actorが1つのタスクのみのアクセスに制御している状態をisolate(隔離)と呼びます。Actorのプロパティやメソッドがisolateされていることで通常の参照とは異なり、awaitを使用して呼び出すことが強制されます。
actorキーワードで定義したオブジェクトのプロパティやメソッドは自動的にisolateされている状態(isolated)になりますが、特定のメソッドに対してisolateしたくない場合が出てきます。これを解決するのがnonisolatedキーワードです。
例えば以下のようなプロパティを特に参照せず処理だけを行うようなoutputメソッドを定義したとして、このままでは外部から呼び出すときにawaitが必要になってしまいます。
actor Score {
private(set) var value: Int = 0
func output(score: Int) {
print("Score:\(score)")
}
}
そこでnonisolatedを付与することでこのメソッドが隔離対象から除外されるのでawaitがなくても呼び出すことができるようになります。
nonisolated func output(score: Int) {
print("Score:\(score)")
}
nonisolatedは定数letには付与できますが変数varには付与することができません。varに付与できてしまうとActorを使用するメリット自体が失われるので当然と言えば当然かもしれません。またletには付与できますが、そもそもActorで定義した定数はisolateされないのでただ明示的に記述するだけになります。
actor Score {
nonisolated let id: Int
nonisolated var category: Int = 0 // 'nonisolated' cannot be applied to mutable stored properties
private(set) var value: Int = 0
/// もちろんisolateされているプロパティを参照しているメソッドはnonisolatedできない
nonisolated func update(score: Int) {
value = score // Actor-isolated property 'value' can not be mutated from a nonisolated contex
print("Score:\(value)") // Actor-isolated property 'value' can not be mutated from a nonisolated contex
}
/// これはOK
nonisolated func output() {
print("ID:\(id)")
}
}
isolated
isolateされないようにするnonisolatedキーワードと対をなすisolateするためのisolatedキーワードも存在します。
例えば引数にActorを受け取るメソッドを定義する際にそのまま実装するとエラーになります。
func calcScore(score1: Score, score2: Score) -> Int {
score1.value + score2.value // Actor-isolated property 'value' can not be referenced from a nonisolated context
}
isolatedキーワードを付与することでエラーを吐かなくなります。isolatedが1つでも付与された関数はisolateされた状態になるため呼び出す際にはawaitが必要になります。
let score1 = Score(id: 0, category: "English", value: 10)
let score2 = Score(id: 0, category: "Mathematics", value: 50)
func calcScore(score1: isolated Score, score2: isolated Score) -> Int {
score1.value + score2.value
}
Task {
print(await calcScore(score1: score1, score2: score2))
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。







