Swift 5.5から導入されたActorは、並行処理におけるデータ競合を防ぐための強力なツールです。しかし、Actorの再入可能性(Reentrancy) によって、意図しない競合状態(Race Condition)が発生する可能性があります。本記事では、具体例を交えながら、Actorの再入可能性と競合状態について詳しく解説します。
登場人物
- 田中先生: 情報科学を教える高校の先生。Swiftの並行処理について詳しい。
- 翔太(しょうた): 高校2年生。プログラミング部に所属し、最近Swiftを学び始めた。
- 彩花(あやか): 高校1年生。プログラミング初心者で、並行処理についてよくわかっていない。
Scene 1: プログラミングの授業開始
(高校の情報科学の授業中。Swiftの並行処理について学んでいる。)
田中先生: 「今日は、Swiftのactor
について説明するぞ。特に、actor
の再入可能性(Reentrancy) について詳しく話す。」
翔太: 「先生!actor
って確か、並行処理でデータ競合を防ぐための仕組みでしたよね?」
田中先生: 「その通り!Swiftではactor
を使うことで、複数のスレッドが同時にデータを書き換える データ競合(Data Race) を防ぐことができる。」
彩花: 「データ競合って、プログラムがバグる原因になるんですか?」
田中先生: 「そうだ。例えば、2つの処理が同じ変数を書き換えようとすると、結果が予測できないことがある。それを防ぐのがactor
だ。ただし……」
翔太: 「ただし?」
田中先生: 「actor
には 再入可能性(Reentrancy) があるため、場合によっては 競合状態(Race Condition) が発生する可能性があるんだ。」
彩花: 「え、どういうことですか?」
田中先生: 「よし、具体的なコードを見てみよう。」
Scene 2: actor
の再入可能性とは?
田中先生: 「まず、次のコードを見てみよう。」
actor Score {
var localLogs: [Int] = []
private(set) var highScore: Int = 0
func update(with score: Int) async {
localLogs.append(score) // ①
highScore = await requestHighScore(with: score) // ②
}
func requestHighScore(with score: Int) async -> Int {
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC) // 2秒待つ
return score
}
}
彩花: 「update
関数は何をしてるんですか?」
田中先生: 「まず、localLogs.append(score)
でスコアを記録して、それからawait requestHighScore(with: score)
でスコアを更新している。」
翔太: 「await
を使うと、処理が止まるんですよね?」
田中先生: 「そう。await
が実行されると、処理が2秒間止まる。この間に、別のupdate
が実行されたらどうなると思う?」
彩花: 「え?もしかして、localLogs.append(score)
の順番が変わる?」
田中先生: 「その通り!それが 競合状態(Race Condition) の発生原因だ。」
Scene 3: update
を2回呼び出したら……
田中先生: 「次のコードを見てみよう。update(with:)
を2回非同期で呼び出している。」
let score = Score()
Task.detached {
await score.update(with: 100) // ③
print(await score.localLogs) // ③'
print(await score.highScore) // ③"
}
Task.detached {
await score.update(with: 110) // ④
print(await score.localLogs) // ④'
print(await score.highScore) // ④"
}
田中先生: 「このコードを実行すると、結果はこうなる。」
[100, 110]
100
[100, 110]
110
翔太: 「なるほど、localLogs
には [100, 110]
って記録されてる……。」
彩花: 「あれ?highScore
は最初100だったのに、最後は110になってますね?」
田中先生: 「そう。このawait
のせいで、実行の順番が変わることがあるんだ。」
Scene 4: await
の前後で結果が変わる
田中先生: 「今度は、localLogs.append(score)
をawait
の後に移動してみよう。」
func update(with score: Int) async {
highScore = await requestHighScore(with: score) // ②
localLogs.append(score) // ①
}
田中先生: 「実行すると、結果はこうなる。」
[100]
100
[100, 110]
110
彩花: 「あれ?さっきと違う!」
翔太: 「await
の位置を変えただけで、結果が変わってる!」
田中先生: 「そう。await
を挟むことで、処理が中断されるから、localLogs.append(score)
が実行されるタイミングが変わるんだ。」
Scene 5: まとめ
田中先生: 「じゃあ、今日のポイントをまとめよう。」
✅ actor
はデータ競合を防ぐが、await
を挟むと別の処理が割り込む可能性がある。
✅ 競合状態(Race Condition)は、await
の位置によって発生し、処理結果が変わることがある。
✅ 並行処理を考慮して、データの更新順序に注意しないとバグの原因になる!
翔太: 「なるほど、actor
が安全だと思っても、await
の使い方を間違えると危険なんですね。」
彩花: 「Swiftの並行処理って、思ったより奥が深いんですね……。」
田中先生: 「その通り!でも、正しく使えばactor
はとても強力なツールだ。今日学んだことを活かして、次はSendable
やglobal actors
について学んでみよう。」
翔太・彩花: 「はい、先生!」
(授業のチャイムが鳴り、プログラミングの授業は終了した。)
【完】