DEV Community

Kevin Galligan for Touchlab

Posted on

Kotlin/Native - Transferring State

Kotlin/Native (KN) has special rules around state and concurrency. We cover them in usable detail in Practical Kotlin Native Concurrency and in more detail in KotlinConf 2019: Kotlin Native Concurrency Explained.

There are basically two rules. Mutable state can only be accessed by one thread at a time, and immutable state can be shared. When sharing state we freeze it, and mutable state generally stays thread confined. However, you can pass mutable state between threads. In all of my discussions, I mostly ignore that possibility, because it's fairly impractical and we don't use it anywhere (in production), but today we'll discuss it a bit.

Transferring State

In order to transfer mutable state between threads, you need to make sure there are no external references to it. The Worker.execute function and the DetachedObjectGraph constructor both take a producer function argument. The purpose of this function is to "produce" the state that you want to transfer, in such a way that all external references can be omitted.

That is the first complication and critical concept to understand. You'll often want to add mutable data to a DetachedObjectGraph from existing state, but this is syntactically difficult.

    @Test
    fun failLocal(){
        val d = Dat("Hello")
        assertFails {
            DetachedObjectGraph {d}
        }
    }

    data class Dat(val s:String)

The DetachedObjectGraph constructor takes a lambda producer argument. In it we try to return d, but d is still referenced from outside the lambda, specifically here in the local val d, so the transfer fails.

This case is overly simplistic. You can "solve" it with the following.

    @Test
    fun directReturn(){
        DetachedObjectGraph {Dat("Hello")}
    }

    data class Dat(val s:String)

The Dat instance returned from the producer has no external references, so you can transfer it. However, use your imagination and extrapolate how difficult it will be to maintain mutable state in a DetachedObjectGraph and pass it around. It quickly becomes impractical.

I would abbreviate DetachedObjectGraph with DOG, but DetachedObjectGraph is not your friend, and dogs are, so I will use the full name DetachedObjectGraph and clutter your visual space because we don't like them. Anyway...

You can build some managed access, with something like the following.

class SharedDetachedObject<T:Any>(producer: () -> T) {
    private val adog :AtomicReference<DetachedObjectGraph<Any>?>
    private val lock = Lock()

    init {
        val detachedObjectGraph = DetachedObjectGraph { producer() as Any }.freeze()
        adog = AtomicReference(detachedObjectGraph.freeze())
    }

    fun <R> access(block: (T) -> R): R = lock.withLock{
        val holder = FreezableAtomicReference<Any?>(null)
        val producer = { grabAccess(holder, block) as Any }
        adog.value = DetachedObjectGraph(TransferMode.SAFE, producer).freeze()
        val retult = holder.value!!
        holder.value = null
        retult as R
    }

    private fun <R> grabAccess(holder:FreezableAtomicReference<Any?>, block: (T) -> R):T{
        val attach = adog.value!!.attach()
        val t = attach as T
        holder.value = block(t)
        return t
    }

    fun clear(){
        adog.value?.attach()
    }
}

You can find this code in a new Stately branch.

This is somewhat complex to look at, but the concept is relatively simple. You create an instance of SharedDetachedObject with a producer, similar to the previous examples. The state returned is kept in a DetachedObjectGraph, and you can access that state with the access method. From that method you can return a value, although make absolutely sure it isn't the mutable state you're holding in the DetachedObjectGraph.

val detachedObject = SharedDetachedObject { mutableListOf("a", "b")}

repeat(50_000){rcount ->
    detachedObject.access {
        val element = "row $rcount"
        it.add(element)
        element
    }
}

The code above creates a mutable list that can be accessed in a shared way, from multiple threads. Anything leaving the access lambda that's still referenced from inside it should be frozen, of course (String gets special treatment in KN. It's always frozen).

OK! Transferring state may be syntactically messy, but it works, right! Sure, but there's another important consideration.

Performance

Transferring state requires that you inspect all of the state you want to transfer, to ensure nobody external is referencing it. All of the state. In the example above, that means on the last run, all 50k entries. Doing that isn't free.

Imagine a common use case. Building a HashMap cache. One of the primary benefits of a HashMap is read and write time. In the good case, they're constant time. Keeping a map in a DetachedObjectGraph sounds like a good idea, but each access of the map is followed by detaching so you can put it back in the DetachedObjectGraph for another thread to use. That turns constant time into linear time. A linear time hash map is a shitty hash map.

In our simple list case above, the time per-insert grows as the list gets larger.

We have a new set of concurrent/mutable state holders coming out for Stately. You can see the current branch here. In the next post we'll cover how to use the new state objects.

Hiring!

Touchlab is hiring! Looking for Android-focused mobile developers, and also for experienced or very interested Kotlin Multiplatform devs. We really need to find a solid Android dev at this precise moment, though. Just FYI. Remote friendly (US-only, for now).

Top comments (0)