Kotlin coroutines and shared mutable state. Part 2

Lukasz Wojtach
6 min readApr 8, 2024

In part 1 we have considered a few methods of taming shared mutable state in a coroutine word. Those methods in general tried to manipulate underlying threads and were not successful in solving problem of seven concurrent dwarves and a single dinner.

(For more specific info regarding our use-case look into part 1)

It is time to try more coroutine-specific solutions.

MutableStateFlow

This entity is probably well known by android developers by now. It is an observable state holder, perfect for exposing an application state to its UI part. MutableStateFlow has also another great property — it allows for atomic updates.

Atomic

But, hold your horses — what does it mean, atomic? We have seen it before, while discussing @Volatile annotation. An atomic operation is such, that it cannot be divided into smaller parts. Look into the getDinner() function above. It involves [reading dinner state + reading wood amount + writing new wood amount + writing new dinner state].

Your processor will perform all those operations separately. Thus, it has no problem performing several read dinner state operations, before it gets to write dinner state.

However, if one could somehow tell your processor, that it needs to execute whole getDinner() function as one, indivisible operation, before performing other operations, like another getDinner() function — then it definitely could have helped seven concurrent dwarves to make just a single dinner and consume it. No dwarf would be able to check the state of dinner, while another dwarf is still busy cooking a new dinner.

MutableStateFlow conveniently has ready to use functions, which allow for atomic updates of its state. They are the equivalent to the ones found on AtomicBoolean, AtomicInteger and so on. So let us try to fix dwarves problem with using MutableStateFlow and its atomic updates:

And how did it go? Likely similar to this result:

Dwarves are about to dine. Wood amount: 7 

Dwarf #1 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #0 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=89, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=89, isReady=true)

Dinner eaten, wood remaining: 0

Great news — our dwarves are finally eating the same dinner. At last we have fixed something. But see the last output line: Dwarves have used all the wood they had, instead of just a single log! How come? They cooked just one dinner, right?

Not exactly. See, an atomic update works like this:

var currentDinner = MutableStateFlow(Dinner.EMPTY)

currentDinner.updateAndGet { dinner ->
if (dinner.isReady) dinner else cook()
// everything above is run without any synchronization
// but below there is an invisible check…
// if (dinner != currentDinner.value) rerun the whole lambda with new current dinner value
}

So it will still let all the dwarves enter the kitchen. Still all the dwarves would see the dinner as not ready and they will start cooking, depleting the wood amount. However only the first dwarf to finish cooking (#1), will see that the input parameter to his updateAndGet lambda has not changed. His dinner will be set as a new value of our MutableStateFlow of currentDinner.

Other dwarves, after they have finished cooking, will notice, that the input parameter for their lambdas is no longer the same, as the currentDinner value. So they will throw their freshly cooked dinners out of the window and start from scratch with the new input parameter — that is the already cooked dinner (by dwarf #1).

Why is it working like that? Thanks to that, such atomic updates are very fast. No synchronisation, locking, etc is required. And as long, as our update lambda does not contain any side effects, it is totally safe. After all, it is the result of update, which matters, not how many times this function was run… unless it contains side effects.

Unfortunately, our dwarves do change the state of the outside world, we do have a side effect inside an update block — we deplete the wood amount. And it shows.

TLDR, MutableStateFlow is a great tool for sharing state — thread safe and mostly “coroutine-safe”. And pretty fast at that. But watch out for any side effects in your update block.

Mutex

Mutex is to coroutines, what synchronised function is to the threads. One cannot lock coroutine on any object, like with a thread. But you can lock coroutine on a Mutex.

When a coroutine locks itself on a Mutex, then every other coroutine, which tries to lock onto the same Mutex instance, will get suspended untill the original coroutine finishes whatever it was doing inside the locked section. Exactly like a synchronised block.

Enough talking, show me the stuff:

Now, when we run this code it should show similar output:

Dwarves are about to dine. Wood amount: 7 

Dwarf #0 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #1 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=6, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=6, isReady=true)

Dinner eaten, wood remaining: 6

All the dwarves are eating the same dinner. And finally they are using just a single wooden log to cook it. Mission accomplished! 🎉

MutableStateFlow vs Mutex

Do you recall:

TLDR, MutableStateFlow is a great tool for sharing state — thread safe and mostly “coroutine-safe”. And pretty fast at that.

Why am I not saying it about Mutex? Well, first issue is that it is not so fast.

You can try running our Mutex code with, say, 700 000 dwarves. It takes about 2,1 second on my laptop:

Kotlin playground may not wish to run such a long running function, better try it on your device. Just copy-paste the code in your IDE.

With MutableStateFlow its about 1,4 second.

Kotlin playground may not wish to run such a long running function, better try it on your device. Just copy-paste the code in your IDE.

Quite a difference. Preemptive synchronisation definitely has its cost.

Second reason is that running into a deadlock while using a Mutex is much easier, than one could think. But that is a topic for another story.

There is also a third case against using a Mutex by default: If you are running your code on a dynamic environment, where coroutines may be canceled at any moment (like on Android), then… look into the next paragraph

Mutex and coroutine cancellation

Now, what happens, when a coroutine inside a mutex.withLock { } block is cancelled? Will our operation remain atomic? Will our state still stay safe?

Let’s find out:

We will once again run our dwarven coroutines on a single thread — to ensure, that we cancel the first dwarf, who manages to enter the kitchen and start cooking a dinner.

The result:

Dwarves are about to dine. Wood amount: 7 

Dwarf #1 is eating dinner: Dinner(ingredients=95, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=95, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=95, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=95, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=95, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=95, isReady=true)

Dinner eaten, wood remaining: 5

Dwarves are eating the same dinner. But they have used two units of wood! Notice, that dwarf #0 (the one, who was making a dinner) is not dining. Evidently the canceled coroutine/dwarf did not finish its work inside the code block protected by Mutex. The dwarf #1 had to finish the job using an additional unit of wood.

So… the Mutex does not protect against premature cancellation. Operation in the locked section is not fully atomic in this regard. And it can lead to state inconsistency.

Non cancellable Mutex

So, how about we just surround the mutex’s locked section with NonCancellable context? Surely it should easily fix our problem with excessive wood usage.

If kotlin playground signals an error due to timeout, try running it locally on your computer.

Result:

Dwarves are about to dine. Wood amount: 7 

Dwarf #0 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #1 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #2 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #3 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #4 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #5 is eating dinner: Dinner(ingredients=65, isReady=true)
Dwarf #6 is eating dinner: Dinner(ingredients=65, isReady=true)

Dinner eaten, wood remaining: 6

And fixed it did. But hold on — dwarf #0 is dining now… while he was supposed to cancel his work and rush to the mine to fix some emergency! If we were in an android world, we would be holding on UI resources (much) longer, that they are needed. Our user has navigated away from this screen!

If you think about it for a moment — we are trying to fix an unfixable situation here. We cannot expect the dwarf/coroutine to both finish its work and promptly cancel to free the resources.

How to achieve both full state safety and not tie up resources in some long-running, non-cancellable coroutine? Check up next part of this story for details.

--

--