Mutual Exclusion

Kotlin Mutex Explained

Michal Ankiersztajn
3 min readMay 27, 2024

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 counter100 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:

Mutex in practice

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:

--

--

Michal Ankiersztajn

Android/Kotlin Developer & Applied Computer Science Engineer