💭

Kotlin/Native new memory managerについて

2022/12/01に公開

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のグローバルなobjectcompanion 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