Mutual Exclusion
Kotlin Mutex Explained
Mutex stands for Mutual Exclusion and solves the problem of accessing shared code sections that shouldn’t run concurrently. It works like a lock with lock
and unlock
function to limit critical sections. Additionally, lock
it is a suspending function, meaning it won’t block a thread.
In short, you should use it when you need to update shared state through multiple Coroutine
s to guarantee the integrity of the data. Let’s take a short look at how it works:
How it works?
Let’s consider a very simple counter-example. We’ll have 2 jobs. Each job will update the counter
100 times, meaning the counter
should be 200 at the end of work.
var counter = 0
fun main() = runBlocking {
val job1 = createCounterJob()
val job2 = createCounterJob()
// Wait for all jobs to finish
joinAll(job1, job2)
// We want the counter to be 200
// Sometimes we'll get 200, but usually it'll be 180, 124 etc.
// There's no guarantee for the right calculations
println(counter)
}
fun createCounterJob() = CoroutineScope(Dispatchers.Default).launch {
for (i in 0..99) {
counter++
}
}
But there’s a problem: most of the time, we won’t get 200, but other results like 180, 143, 124, etc. In other words, there’s no guarantee that the final counter
will be updated 200 times.
We can fix it by adding Mutex
to a critical section of the code. To simplify things further, we’ll use withLock
that uses lock
at the start of the code block and unlock
at the end:
var counter = 0
// Add a mutex used by all Coroutines
val mutex = Mutex()
fun main() = runBlocking {
val job1 = createCounterJob()
val job2 = createCounterJob()
// Wait for all jobs to finish
joinAll(job1, job2)
// Counter = 200 every time
println(counter)
}
fun createCounterJob() = CoroutineScope(Dispatchers.Default).launch {
for (i in 0..99) {
// Place of change, the same Mutex reference must be used
mutex.withLock { counter++ }
}
}
Now, the Coroutine
will suspend until the critical section is completed by a separate Coroutine
. We can visualize a situation where Job1
blocks access to counter++
code section and forces Job2
to suspend:
Advanced Example
Mutex is a handy concept. It can be used by individual Lock
reference to block different parts of code differently.
Here’s an example of ImageFactory
that caches images and uses Mutex
to avoid additional calls and block coroutines until the image is under the same url
is cached:
class ImageFactory {
private val cache = mutableMapOf<String, Image>()
private val locks = mutableMapOf<String, Mutex>()
private val lock = Mutex()
suspend fun get(url: String): Image {
val imageMutex = lock.withLock {
locks.getOrPut(url) { Mutex() }
}
val image = imageMutex.withLock {
getImage(url)
}
lock.withLock { locks.remove(url) }
return image
}
private suspend fun getImage(url: String): Image =
cache[url] ?: fetchImage(url).also { image -> cache[url] = image }
}
Note that getOrPut(url)
needs to be inside a lock
, or else we could run into an issue where 2 Coroutine
s put a new lock
under the same URL.
Reference: