· 7 min read Posted by Touchlab

Kotlin/Native Concurrency

A mini-tutorial together to help you better understand the state rules of Kotlin/Native, including simple, frozen, global and advanced threading.

We put this mini-tutorial together to help you better understand the state rules of Kotlin/Native. For a more in-depth, step-by-step, tutorial, see our Kotlin/Native Concurrency hands-on lab published on the official Kotlin site.

You can also watch Touchlab partner Kevin Galligan’s talk at Kotliners 2020 on this very topic. 

Summary
Kotlin Multiplatform will allow sharing native code among many platforms. The native mobile developer world, in particular, will see a lot of benefit very quickly from KMP. The Android developer community seems pretty excited by the possibilities. However, coming from the JVM, there are new things you must learn. One of the most important is Kotlin/Native’s state and concurrency model. While the Kotlin/Native state model is conceptually simple, it is also different, and will cause some confusion if you don’t learn the rules.

His talks covers: 

  • How the state and concurrency model work
  • Why the Kotlin team made these design choices
  • Libraries to help
  • Patterns to follow
  • What to do when you have trouble

He doesn’t get into the expert level topics, but you’ll have all the info you need to get started with Kotlin/Native and concurrency. 

Simple State

We can get started with Kotiln/Native by focusing on the simplest case without global state, without crossing threads – just standard, local class instances.

For example:

fun runSimpleState(){
    val s = SimpleState()
    s.increment()
    s.increment()
    s.report()
    s.decrement()
    s.report()
}

class SimpleState{
    var count = 0

    fun increment(){
        count++
    }

    fun decrement(){
        count--
    }

    fun report(){
        println("My count $count")
    }
}

This will print:

My count 2
My count 1

Regular class state that is not frozen (see next section) remains mutable and behaves as you’d normally expect it to. If you’re not using global values or crossing threads, Kotlin/Native is just like the other Kotlin flavors.

Frozen State

In Kotlin/Native, all state is considered mutable unless it is frozen. Frozen state is a new concept for Kotlin, although it exists in some other languages. In Kotlin/Native, there is a function freeze() defined on all classes. When you call freeze(), that object and everything it touches is frozen and immutable.

Once frozen, state can be shared between threads, but it cannot be changed.

Understand that calling freeze() will change an attribute of your state at runtime. There is a flag on every object in Kotlin/Native that says if it is frozen or not. You can check if some state is frozen be reading the isFrozen property.

After we freeze, if you try to change the value, it’ll throw an InvalidMutabilityException.

When you see InvalidMutabilityException, it means you are attempting to change some state that has been frozen. You probably did not want this state to be frozen, so your job will be to figure out why, and more importantly when, it was frozen.

It’s important to understand that freezing an object means you’ll also be freezing everything that object has a reference to.

fun freezeChildren(){
    val dataWithReference = DataWithReference(SomeData("Hello 🐶", 22))
    dataWithReference.freeze()

    println("Am I frozen? ${dataWithReference.child.isFrozen}")
}

data class DataWithReference(val child:SomeData)

DataWithReference has a child property val child:SomeData. When you freeze the parent, the child is frozen. This will be important to understand when we start working with larger object graphs.

Once you’ve rearchitected code for safe concurrency, you’ll start to see how potentially unsafe a lot of the JVM code is. Enforcing safe mutability is generally a good thing.

Global State

Some things in Kotlin can be referenced globally, from any thread. Kotlin/Native state rules apply to everything, so global state has some special rules applied out of the box.

There are two types of global state we need to be concerned with: objects and properties.

object

All global object instances are frozen, but they can be referenced from any thread.

If you need that state to be mutable you do have some options. You can use atomics, which we’ll talk about later, but you can also make that object thread local by annotating the object with @ThreadLocal.

That means each thread has its own copy, and according to rule #1, that means it can be mutable. Each thread gets its own copy of an object annotated with @ThreadLocal.

Since each thread gets its own copy of an object annotated with @ThreadLocal, mutating an object in one thread will not affect others.

In this example:

fun threadLocalDifferentThreads(){
    println("main thread: i ${ThreadLocalGlobalState.i}")
    ThreadLocalGlobalState.i++
    println("main thread: i ${ThreadLocalGlobalState.i}")
    background {
        println("other thread: i ${ThreadLocalGlobalState.i}")
    }
}

@ThreadLocal
object ThreadLocalGlobalState{
    var i = 5
}

The result will be:

main thread: i 5
main thread: i 6
other thread: i 5

Properties

Global properties are accessible only from the main thread, but are mutable.

When you access a global property from a background thread, you’ll see IncorrectDereferenceException. It basically means you’re not allowed to touch that state, as in the case here. The code compiles, but the runtime says you can’t touch that global property from a background thread.

For native mobile developers, this should be very familiar. You’re not allowed to edit the UI state from a background thread. This is again an example of how Kotlin/Native’s runtime is simply formalizing a rule we’re all generally familiar with.

Like with object, you can annotate a global property with @ThreadLocal. This will give each thread a copy, and obviously will allow you to access that state from other threads. You can also annotate global properties with @SharedImmutable, which will make that property available to all threads, but it will be frozen, like object is by default.

Advanced Threading

In Kotlin/Native, if you are directly managing your concurrency, you’ll probably be using WorkerWorker is Kotlin/Native’s concurrency helper. Each Worker has its own thread internally, and you can schedule work to be done on that thread.

While you can technically attempt to pass mutable state to other threads, in practice, Kotlin/Native concurrency libraries will automatically freeze state when passing between threads. A general model of Kotlin/Native concurrency libraries has emerged. To perform work in a different thread, you pass a lambda to a function. That lambda, and all of the data is captures, is frozen.

Local values are easy to understand and deal with. Where this becomes more problematic, and triggers more controversy, is when you wind up capturing more state than you had intended. To help avoid this, it is good practice to move thread-jumping code to another function, and only capture function arguments so the parent doesn’t get frozen.

Debugging

InvalidMutabilityException

The issue you will most likely encounter is InvalidMutabilityException, when attempting to edit frozen state. Fixing this means figuring out how the state was frozen. The Kotlin/Native runtime provides a function ensureNeverFrozen()which can be called on any object. If called on a frozen object, it will fail immediately. If called on an unfrozen object, if that object is later being frozen, the freezing process will fail.

As mentioned earlier, you can pass a lambda to a function to perform work on a different thread. When you do that, the lambda and all the data it captures is frozen. This can be the cause of something getting frozen unintentionally. By calling ensureNeverFrozen() on an object before that happens, you can see what the cause is.

You may call ensureNeverFrozen() in the init block for objects you’re sure should never be frozen, especially during early development of a project.

The stack trace generated when you try to freeze an object that should never be frozen is a bit verbose, but you’ll be able to walk back to the source issue.

IncorrectDereferenceException

IncorrectDereferenceExceptionwill occasionally come up when you use a library or some new shared code that was tested on the main thread, but you run it on a background thread for the first time.

You will also see it if you move unfrozen state between threads outside of Kotlin/Native. For example, creating a data class in Swift on the main thread and passing it to a Kotlin function called on a different thread will result in IncorrectDereferenceException. You’ll need to freeze it before crossing threads, either in Kotlin or by exposing a method to freeze data to Swift. The same rules will apply if integrating Kotlin/Native and any native platform code, including C interop code.

If you’re crossing threads in native code, you need to be very careful with what state is touched by different threads, and any of that needs to be frozen.