Kotlin/Native new memory managerについて
Kotlin 1.7.20からデフォルトで使用可能なnew memory managerについて調べてみました。
なぜnew memory managerが必要なのか?
Kotlin Multiplatform Mobile(KMM)では、AndroidとiOS両プラットフォームでKotlinのソースコードを共有して開発をすることができます。
AndroidとiOSそれぞれ別々に管理して、開発を進めていればそれぞれのプラットフォームのことだけを考えて開発をするめることができますが、KMMではプラットフォーム共通の処理で若干書き方が普段と違ってきます。
new memory managerになったことによって既存のめんどくさかった部分が改善されたので、今回はその部分をみていきます。
以前のmemory manager
具体的に何が変わったのかを見る前に、まずは元々どのような動きになっていたのかをみていきます。
freeze
mutableなオブジェクトを複数のスレッドで動かすことは、危険であるため従来のmemory managerではmutableなオブジェクトに対して、freeze()
してimmutableにしてから別のスレッドに共有する必要がありました。
では、どのようなオブジェクトがmutableと判断されるのでしょうか?
Kotlinのval
ではimmutableであるとは判断されず、freeze()
することでimmutableと判断されます。
以下の例では、immutableなオブジェクトとして判断されます。
data class User(val name: String, val age: Int)
しかし次のコードはimmutableではなくmutableと判断されます。
interface UserInfo {
val weight: Int
val height: Int
fun demo() {
......
}
}
data class User(val name: String, val info: UserInfo)
なぜ、immutableだと判断されないかというと、UserInfo
がInterfaceになっているからです。Interfaceは、コンパイル時にimmutableであると判断することができないので、immutableと判断されません。
実行時に必ずimmutableであると判断されなければならないのです。
この問題を解消するためには、freeze()
を使う必要があります。
freeze()
を使うと、frozen
という状態になりfrozen
状態になったオブジェクトは変更することができなくなります。
もしfrozen
状態になったオブジェクトを変更しようとするとInvalidMutabilityException
が発生するようになっています。
なので、frozen
状態であれば必ずimmutableな値であると判断することができるようになります。
またfrozen
状態のオブジェクトであれば、そのオブジェクトが参照しているオブジェクトもfrozen
つまりimmutableと判断されます。
なので、下記の例では2つともimmutableと判断されます。
data class UserInfo(val weight: Int, val height: Int)
data class User(val userInfo: UserInfo, age: Int)
val user = User(UserInfo(60, 180), 20)
user.freeze()
また、isFrozen
を使うことでfrozen
状態かどうかを確認することもできます。
Global
Kotlinのグローバルなobject
やcompanion object
はデフォルトからimmutableであると判断されますが、下記のような場合例外が投げられます。
object User {
var name = "demo"
fun changeName() {
name = "new name"
}
}
User
はimmutableですが、内部のname
はmutableな値になっています。これにより例外が発生します。
この問題を解消するためには、@ThreadLocal
アノテーションをつける必要があり、このアノテーションをつけると、コピーしてアクセスしてくる各スレッドに、コピーしたオブジェクトを渡すようになります。
@ThreadLocal
object User {
var name = "demo"
fun changeName() {
name = "new name"
}
}
ThreadLocal
を使用すると、別のスレッドからも参照することができるようになっていますが、この時にアクセスしたスレッド間で別の値を持つ可能性もある(コピーしたものを参照することになるので)のでそこは注意が必要になります。
また、グローバル変数にもこの@ThreadLocal
を使用することで、immutableな値としてメインスレッド以外からもアクセスすることができるようになります。
グローバル変数の場合は、SharedImmutable
をつけると他のスレッドからもアクセスできるようになりますが、frozen
状態になるので変更はできなくなります。
@SharedImmutable
val globalUser = User("demo", 20)
globalUser.name = "foo" // crash!
new memory managerで何が改善されたか?
new memory managerによってスレッド間のオブジェクト共有に関する制限がなくなりました。
上で紹介したように、バックグラウンドスレッドで動作していたオブジェクトをfreeze()
を使うことなく、マルチスレッドで動作させることができるようになりました。
これにより、freeze()
してimmutableになっているかどうかを以前より気にすることなく開発を進めることができるようになりそうです。
新しく、new memory mangerを使用して開発を進めていくとfreeze()
の部分を消すことができるようになるので、よりシンプルでみやすくなり、何より気を使う部分が減るので開発効率が上がると思います。
ただ、AtomicReferenceクラスを使用している場合と、freeze()
することが条件となっている処理の場合のみ、注意が必要になります。
AtomicReferenceを使用している場合は、freeze()
を使用する必要があります。
また、@ThreadLocal
アノテーションと@SharedImmutable
アノテーションがなくてもグローバルな値を共有することができるようになりました。
ただし、注意点として@ThreadLocal
と@SharedImmutable
をつけた際の挙動、そしてfreeze()
を使った時の挙動に関しては変更していないので、そこには注意が必要になります。
data class User(var name: String)
val user = User("demo")
user.freeze()
user.name = "foo" // crash!
最後に
new memory managerがKotlin 1.7.20からデフォルトで有効になったことで、よりKMMの開発が進めやすくなったと思います。
Kotlinでさまざまなプラットフォームでの開発を進めていきましょう!🙌
参考
Kotlin Native. New Memory management Model
Try the New Kotlin/Native Memory Manager Development Preview | The Kotlin Blog
Kotlin Multiplatform for Cross-Platform Mobile Development
Concurrency overview
Discussion