In the first part of the series, we have seen, that accessing mutable state from multiple concurrent coroutines generates more or less the same problems, as it does with threads. However techniques used to “tame” mutable state sharing among threads, do not really work for coroutines. Moreover — even if we run our coroutines on a single thread, we may still experience problems with our mutable state consistency.

In the second part we talked about MutableStateFlow and Mutex . Both of them can help us in keeping mutable state safe, but they have their limitations and can still fail us in cases similar to our dwarven dinner problem.

State-holding coroutine

Previously we came to the conclusion, that it is impossible to have our dwarf/coroutine be both performing safe, uninterruptible state update and be cancellable if need be. But what if we had a separate coroutine tasked with the sole task of keeping and mutating our state?

Let’s name this separate coroutine a Snow White Princess to keep with our fairy tale convention. Now, our dwarves do not enter the kitchen at all. Each of them can communicate its dinner preferences to the princess and wait for its share of the dinner.

The princess is a smart lass. She knows, that one dinner is enough for all the dwarves. Even if a dwarf, or even a few dwarves, are recalled from home to the mine — Snow White can continue cooking the dinner, for she knows it will be needed anyway at some point in the future.

Unfortunately there is no class in the coroutines library, which could play the part of the princess. But we can code it ourselves. First let us design the API of such StateCoroutine. IMHO a sensible decision would be to go along the lines of a DataStore interface from android — as we need the same safety guarantees, that the DataStore provides. That is:

  • Read-after-write consistency.
  • Writes are serialised.
  • Reads are never blocked by writes.

So here we go:

class StateCoroutine<T> {

val state: StateFlow<T>

suspend fun updateState(transform: suspend (T) -> T): T
}

Implementation details are worthy a separate story — let’s concentrate on how such StateCoroutine might be useful to us. With such API we can use it very similar to the MutableStateFlow:

Once again, as with the cancellable Mutex case we will run our coroutine-powered dwarves on a single thread to ensure, that Dwarf #0 is always first back home. Then, after 10 milliseconds we will recall Dwarf #0 to the mine.

Results:

Dwarves are about to dine. Wood amount: 10 

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

Dinner eaten, wood remaining: 9

And voila! Dwarf #0 is not dining. He rushed to the mine immediately, as he had no other things to do, while waiting for the dinner to be made. This however did not affect state in the kitchen. Snow White princess, being a separate coroutine, continued to cook a dinner on a single wooden log. And all the dwarves were later given the same dinner.

As for performance, such a StateCoroutine is about as performant as Mutex and carries a similar risk of a deadlock. However, as we are using separate coroutine for state updates, we have successfully accomplished a need of not tying unexpected resources for long and our update not being interrupted by a cancellation.

Sum up

Our journey through various techniques of sharing a mutable state between coroutines is now complete. Should you now always use aforementioned StateCoroutine for sharing state? Of course not. My advised solution would be to use MutableStateFlow for usual mutable state sharing scenarios and such StateCoroutine for when you cannot get rid of side effects from your state-update logic.

The end :)

--

--