Kotlin Nativeと並行処理
Kotlin Nativeの並行処理では制限がいくつかあります。そのあたりに色々調べてみたので、まとめておきたいと思います。この問題に対応した新しいメモリマネージャーが現在Experimentalで使用できますが、今回は使っていない状態のものになります。
新しいメモリマネージャーがすでありますが、現状を把握しておきたかったので今回調べました。新しいメモリマネージャーはまた別途調査したいと思います。
並行処理におけるルール
Kotin Nativeにおいて並行処理をする際のルールがあります。
- Mutable Stateは1スレッドのみ
- Immutable Stateは複数スレッド可能
Mutable Stateは1つのスレッドでしか扱えません。逆にImmutable Stateは複数スレッドから扱えます。
Kotlin NativeじゃなくてもMutable Stateを並行処理で扱うと問題が起きやすいです。
Kotlin Nativeにおいては並行処理で状態を扱うときはImmutable Stateであることを保証するために、Frozenというランタイム状態を定義しています。
Frozen
Kotlin Nativeにおいて並行処理で状態を扱うときは必ずFrozen状態にする必要があります。
Frozenのものは変更不可になり、変更しようとすると InvalidMutabilityException
が発生します。
Frozen状態にするには、 freeze
メソッドを使うだけです。
オブジェクトがFrozenかは isFrozen
を使って知ることができます。
freeze
メソッドは対象のオブジェクトが参照してるオブジェクトも推移的にすべてFrozen状態にします。
Frozen状態は元に戻すことはできません。
Worker
Kotlin Nativeで実際に並行処理を見ていきたいと思います。Kotlin Nativeでは Worker
という低レベルAPIが用意されているので、これを使っていきます。
使い方は以下のようになっています。
しかし、これを実行しても IllegalStateException
が発生します。
この例だと foo
という状態を入力に使っていますが、このとき foo
はFrozen状態ではありません。Workerは渡された状態がFrozenかをチェックしているため、ここではエラーになってしまいます。
では、どうすればよいのか言うと、 foo
をFozenにすればよいだけです。
これでエラーが発生せず実行できます。
もちろんFrozenなオブジェクトなので別スレッドで input.i = 2
のような変更処理はできません。
Global State
グローバルな状態においてはいくつか自動的にルールが適用され、扱いに注意が必要です。
object
object
で定義されたものは自動的にFrozen状態になります。もちろんFrozenなので変更しようとすると InvalidMutabilityException
が発生します。
object
は @ThreadLocal
をつけることでFrozen状態は解除されます。
ただし、この状態で別スレッドで実行する場合は更に注意が必要です。
これを実行すると、メインスレッドと別スレッドでは、結果が異なっています。@ThreadLocal
を使うと別スレッドで扱うときはコピーが作成されるためメインスレッドとは別のものになります。
companion object
についても同様の扱いになります。
Global properties
グローバル変数にも制限があります。
グローバル変数を別スレッドからアクセスすると IncorrectDereferenceException
が発生します。状態変更しなくても参照するだけでエラーになります。
これに対応するには2つ方法があります。
まずは object
と同じようにグローバル変数に @ThreadLocal
をつけます。もちろん別スレッドにおいてはコピーになるため別の値になります。
もう一つは、 @SharedImmutable
をつけます。これで別スレッドから参照できるようになりますが、状態の変更はできません。
ensureNeverFrozen
ensureNeverFrozen
を使うとオブジェクトがFrozen状態にされてる場合や、Frozenされたタイミングでエラーにすることができます。これを使うとFrozenされた原因などを調べることができます。
Atomic
Frozen状態でも値の変更が可能な Atomic
クラスが提供されています。 AtomicInt
, AtomicLong
, AtomicReference
があります。
AtomicInt
を使った例です。
Coroutines
これまで低レベルAPIの Worker
使っていましたが、高レベルAPIとしてはCoroutinesを使っていくと思います。
native-mt
現在、通常のCoroutinesはKotlin Nativeではマルチスレッドにはならずシングルスレッドとして実行されます。マルチスレッドに対応した native-mt
という特別なバージョンがあるので、そちらを使っていくことになります。
スレッドの確認
動作確認をするために今どのスレッドで実行しているかを確認する方法を書いておきます。
Stately
というライブラリを使います。
これを使って以下のようなプロパティを定義します。この isMainThread
というのでメインスレッドで実行されているかを確認できます。
これはハンズオンのコードから持ってきています。
native-mtではないバージョンの動作確認
native-mt
ではないバージョンを使ってみます。
結果から見てわかるように、 Dispatchers.Default
を使っていてもメインスレッドで実行されています。また、オブジェクトはFrozen状態にはなっていません。
native-mtバージョンの動作確認
native-mt
を使った場合は、別スレッドで実行されているのが確認できます。ただし、参照してるオブジェクトは自動的にFrozen状態になっているので注意が必要です。これは仕様になっています。
また戻り値にも同様にFozenになっています。
参考
- https://kotlinlang.org/docs/native-immutability.html
- https://kotlinlang.org/docs/multiplatform-mobile-concurrency-overview.html
- https://play.kotlinlang.org/hands-on/Kotlin%20Native%20Concurrency/00_Introduction
- https://speakerdeck.com/satoshun/kotlin-nativeniokerufrozenzhuang-tai-tobing-xing-chu-li-nituite