【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からの編集を受け付けないようにするといった排他制御を行う必要があります。
データの競合を実際に発生させてみる
例えば以下のようなクラスがあるとします。
このクラスのvalue
プロパティをさまざまなスレッドから同時に更新処理を実行してみます。期待としては順番はさておきScore:200
、Score:300
、Score:400
となるように思います。
しかし実行してみると出力は以下のようになりました。今回はたまたま400
でしたが全て200
になったり、300
になったりもします。
これを防ぐための手段としてActor
が活用できます。
Actorとは?
Actor
もSwift Concurrencyから提供されている機能の1つで、データの排他制御を行うことができるスレッドセーフなオブジェクトです。Actor
はクラスや構造体と同じような振る舞いを持つ型で、クラスと同じく参照型のオブジェクトになります。
Swift ConcurrencyのTask
などを使用することで並行処理が安全に実行できるようになりましたが共有のデータを操作する際にはデータ競合のリスクが発生します。Actor
は並行コード間で安全にデータを操作するためのオブジェクトで、インスタンスには1つのタスクのみがデータにアクセスできるようになっています。
Actor
はactor
キーワードを使用して定義します。先ほど発生していたデータの競合のコードをActor
に置き換えてみます。
Actor
のメソッドを呼び出す場合はawait
をつける必要があります。
出力が期待通りに動作するようになりました。
文法はクラスと一緒
先述しましたがActor
はクラスと同じく参照型のオブジェクトでした。基本的な文法はクラスと同じで、プロパティやメソッドの定義、イニシャライザの実装、プロトコルの継承なども可能になっています。
ただしActor
を継承してサブActorを作るといったことはできないようになっています。
プロパティやメソッドの呼び出しは非同期
Actor
に定義したプロパティやメソッドを外部から呼び出す場合は自動的に非同期になります。そのためawait
キーワードを使用する必要があり、外部からの参照では別タスクが呼び出し中であればそのタスクの完了を待ってから実行されることになります。
ただし内部から呼び出す場合は非同期を意識することなく普通に参照することが可能になっています。
nonisolated
Actor
が1つのタスクのみのアクセスに制御している状態をisolate
(隔離)と呼びます。Actor
のプロパティやメソッドがisolate
されていることで通常の参照とは異なり、await
を使用して呼び出すことが強制されます。
actor
キーワードで定義したオブジェクトのプロパティやメソッドは自動的にisolate
されている状態(isolated
)になりますが、特定のメソッドに対してisolate
したくない場合が出てきます。これを解決するのがnonisolated
キーワードです。
例えば以下のようなプロパティを特に参照せず処理だけを行うようなoutput
メソッドを定義したとして、このままでは外部から呼び出すときにawait
が必要になってしまいます。
そこでnonisolated
を付与することでこのメソッドが隔離対象から除外されるのでawait
がなくても呼び出すことができるようになります。
nonisolated
は定数let
には付与できますが変数var
には付与することができません。var
に付与できてしまうとActor
を使用するメリット自体が失われるので当然と言えば当然かもしれません。またlet
には付与できますが、そもそもActor
で定義した定数はisolate
されないのでただ明示的に記述するだけになります。
isolated
isolate
されないようにするnonisolated
キーワードと対をなすisolate
するためのisolated
キーワードも存在します。
例えば引数にActor
を受け取るメソッドを定義する際にそのまま実装するとエラーになります。
isolated
キーワードを付与することでエラーを吐かなくなります。isolated
が1つでも付与された関数はisolate
された状態になるため呼び出す際にはawait
が必要になります。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。