Kotlin coroutines and shared mutable state. Part 1

Lukasz Wojtach
5 min readApr 4, 2024

This story is about sharing mutable state between concurrent coroutines. We will start with the methods which do not work (as expected or at all) and progress to more fail-proof solutions.

First we need a use case — welcome our seven concurrent dwarves trying to cook a single dinner (our mutable state) using wood (another mutable state) to keep the fire going in the kitchen.

Dwarves would like that only the first among them, who enters the kitchen, would make a delicious dinner for all. Cooking a diner requires burning a single wooden log and some time (70 milliseconds — about the same as your average network request from mobile device). Since only the first dwarf would be cooking a dinner, only a single wooden log should be used in such approach. All dwarves should be sharing a single, fresh dinner. We can recognise, whether dwarves are eating the same dinner, by the ingredients used to make a dinner.

Before dwarves come home, we will print out the amount of wood they have in kitchen. Afterwards all the seven dwarves will concurrently try to access the kitchen, obtain a dinner and eat it. This they will perform inside a coroutineScope so that we will know, when all of them have finished.

In the end we will check how much wood they have used for cooking

This is the result of running our function:

You can run it yourself:

The printed data should look like this one below:

Dwarves are about to dine. Wood amount: 7 

Dwarf #0 is eating dinner: Dinner(ingredients=47, isReady=true)
Dwarf #1 is eating dinner: Dinner(ingredients=64, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=77, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=8, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=67, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=58, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=30, isReady=true)

Dinner eaten, wood remaining: 0

Dwarves are indeed concurrent and eating in no particular order. Each of them ate different dinner (each dinner has different ingredients). And they have used all the wood available — instead of just a single log.

How did this happen? It’s actually pretty simple. Cooking a dinner takes 70 milliseconds. Since all of the dwarves entered kitchen at once — before those 70 milliseconds have passed — each of them saw, that dinner is not yet ready. And each of them made and ate a separate dinner as a result.

How can we fix this situation? This is actually a surprisingly difficult challenge.

Volatile

First idea to consider is the oldest trick in the book:

suspend fun getDinner() = synchronized(this) {
if (currentDinner.isReady) currentDinner
else cookNewDinner().also { currentDinner = it }
}

Using synchronized in such case is not allowed by the compiler, so we can throw it away immediately.

So maybe we could just put a @Volatile annotation over our mutable values? It…

Marks the JVM backing field of the annotated var property as volatile, 
meaning that reads and writes to this field are atomic
and writes are always made visible to other threads.

Let’s run it!

The results however are not encouraging:

Dwarves are about to dine. Wood amount: 7 

Dwarf #0 is eating dinner: Dinner(ingredients=0, isReady=true)
Dwarf #1 is eating dinner: Dinner(ingredients=45, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=80, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=55, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=9, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=23, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=93, isReady=true)

Dinner eaten, wood remaining: 0

Why so? We can believe documentation, that reads and writes are now atomic (we will get to what it means later), but an operation consisting of [read + wait for 70 ms + write] is not. All of our dwarves still come to the kitchen at once and notice that dinner is not yet ready.

Thread confinement

So how about we limit our coroutines to act only on a single thread? Like, for example, coroutines run within viewmodelScope or lifecycleScope on Android. This is actually a good question. Official coroutines guide would make us believe, that confining coroutines executions to a single thread, would make our shared state safe. After all two coroutines on the same thread cannot execute both at the same time…

…but not in this case. Try it:

Results:

Dwarves are about to dine. Wood amount: 7 

Dwarf #0 is eating dinner: Dinner(ingredients=26, isReady=true)
Dwarf #1 is eating dinner: Dinner(ingredients=9, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=92, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=36, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=82, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=69, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=79, isReady=true)

Dinner eaten, wood remaining: 0

Dwarves start to order themselves neatly, while getting dinner from the kitchen. Clearly being on a single thread does have an effect. But each of the dwarves is still eating different dinner. How is this possible?

The keyword here is suspension . As long as a coroutine does not suspend, it will follow behaviour of the thread (that is why dwarves go to the kitchen in strict order of coroutines being created). But, when coroutine suspends, it yields the underlying thread for other coroutines to use. That is why coroutines “do not block a thread”, are “non-blocking” .

But, as coroutine #1 yields a thread for coroutine #2, this #2 coroutine will start executing BEFORE coroutine #1 ends its execution. This fact makes both coroutines concurrent, despite being run on a single thread. It also means, that, after coroutine suspends, one may throw out of the window all of the single-thread consistency guarantees.

Our dwarves come to the kitchen in order. But as soon as dwarf #1 gets a fire going and suspends while waiting for dinner to be cooked, dwarf #2 enters the kitchen and sees, that dinner is still not ready. So he, too, gets a fire going and suspends, giving a chance for dwarf #3 to enter the fray and notice, that dinner is still not ready yet.

One could say “But I do not use delay() function in my code like that”. Sure. Making an online request, writing to database or doing just about anything on a different thread will also suspend your coroutine (as can be noticed by neat markings in the IDE) and give the same effect.

Thread confinement (confining a block of execution to a single thread) is a very useful and powerful technique. You also kind of get it out of the box, while using things like viewmodelScope from Android ViewModel class (coroutines in this scope are confined to execute on Android Main Thread). But this does not mean unfortunately, that mutable state shared between concurrent coroutines is safe from unexpected behaviour.

In the next part we will explore more coroutine-specific ways to deal with situations, when we have to share some mutable state between concurrent coroutines.

--

--