0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【物語で学ぶ Swift Concurrency】actor の「再入可能性と競合状態」を楽しく理解しよう!

Last updated at Posted at 2025-02-20

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はとても強力なツールだ。今日学んだことを活かして、次はSendableglobal actorsについて学んでみよう。」

翔太・彩花: 「はい、先生!」

(授業のチャイムが鳴り、プログラミングの授業は終了した。)

【完】

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?