What is “concurrent” access to mutable state?
Hopefully, every developer knows that concurrent access to shared mutable state is bad. This access should be synchronized or, even better, avoided altogether or all sorts of hard-to-debug problems are guaranteed. Everyone has a clear picture of what a mutable state is, but there is a lot of confusion and misunderstanding of what concurrent and synchronized mean in this context.
For example, consider the following snippet of Kotlin code using Kotlin coroutines:
val m = mutableMapOf<Int,String>()
m[1] = "one" // (1)
yield() // or other suspending function
m[2] = "two" // (2)
We have a mutable map m
that is mutated twice and an invocation of suspending function in between mutations. The coroutine running this code will get suspended after line (1) and might get resumed in a different thread to execute line (2). We have not synchronized our mutable map in any way, so now the same map is shared between two different threads. Are we doomed?
No. In fact, the implementation of suspending function yield
performs synchronization for us to ensure that code before yield
happens before the code after yield
so that it is not concurrent (i.e. sequential).
So what is concurrent anyway? This word has a precise definition. Two operations are concurrent if they are not ordered by happens before relation. When we execute code on two different threads it does not matter how much actual time passes in between two operations. Concurrency is not defined in terms of a wall clock time. We can have operations (1) and (2) executing ten seconds apart on different threads and they would still be concurrent and cause us all sort of bugs unless there is a happens before relation between them.
But where does happens before relation come from? Well, when we write a single-threaded code all actions in a thread happen in a program order, so all actions in a given thread are ordered, thus sequential. It is always safe to mutate from a single thread. No two actions in the same thread can be concurrent.
However, when we have multiple threads, all actions in different threads are concurrent unless synchronization operations are used. Synchronization operations establish happens before relation between operations in different threads, allowing us to mutate shared state in a sequential way, without introduction concurrent mutations.
Even though a coroutine in Kotlin can execute on multiple threads it is just like a thread from a standpoint of mutable state. No two actions in the same coroutine can be concurrent. And just like with threads you should avoid sharing your mutable state between coroutines or you’ll have to worry about synchronization yourself.
Avoid sharing mutable state. Confine each mutable object to either a single thread or to a single coroutine and sleep well.
Further reading
Formal definition of happens before relation for JVM world can be found in Chapter 17 of JLS. This concept has been introduced by Leslie Lamport in his 1978 paper on Time, Clocks and Ordering of Events in a Distributed System and it has become a standard formal framework to discuss concurrency in all kinds of software systems.