Does the Kotlin Coroutine Mutex Preserve the Call Order?

Jul 31, 2023

I needed to implement a queue that ensures that function calls are executed exclusively, one-by-one, in first-come-first-served order. So, I was wondering: Can I just use Mutex and be done with it? Is the call order in which the lock is acquired preserved, and then the lock granted in order of the calls?

Let’s write a small example to test it out:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

fun main() {
    runBlocking(Dispatchers.IO) { // use Dispatchers.IO so multiple threads are involved.
        val mutex = Mutex()

        repeat(10) { coroutineIndex ->
            launch {
                delay(coroutineIndex * 100L) // ensure lock is acquired in order.
                println("$coroutineIndex acquires lock...")
                mutex.withLock {
                    delay(1000) // delay so long that all launched coroutines tried to acquire the lock.
                    println("$coroutineIndex executes")
                }
            }
        }
    }
}

If the Mutex enqueues the calls, then we can expect that the execution order is sequential from 0 to 9. And this is also what the program outputs:

0 acquires lock...
1 acquires lock...
2 acquires lock...
3 acquires lock...
4 acquires lock...
5 acquires lock...
6 acquires lock...
7 acquires lock...
8 acquires lock...
9 acquires lock...
0 executes
1 executes
2 executes
3 executes
4 executes
5 executes
6 executes
7 executes
8 executes
9 executes

It looks like Mutex actually enqueues and then executes in the correct calling order.

However, since not even the Mutex documentation mentions this behavior, it’s probably not safe to rely on this behavior.

If we speed things up (no delay before lock acquisition), then it already looks different:

fun main() {
    runBlocking(Dispatchers.IO) {
        val mutex = Mutex()

        repeat(10) { coroutineIndex ->
            launch {
                println("$coroutineIndex acquires lock...")
                mutex.withLock {
                    delay(100)
                    println("$coroutineIndex executes")
                }
            }
        }
    }
}

Output:

0 acquires lock...
1 acquires lock...
2 acquires lock...
3 acquires lock...
4 acquires lock...
5 acquires lock...
6 acquires lock...
7 acquires lock...
8 acquires lock...
9 acquires lock...
0 executes
1 executes
8 executes
3 executes
2 executes
9 executes
6 executes
7 executes
5 executes
4 executes

We see that the locks are acquired in order (although not even this is guaranteed, since each coroutine may run on a different thread where one might be executed earlier even when triggered later). So what we have to look for is if the acquisition order equals the execution order, which it clearly doesn’t. The execution order is more or less random. For every execution, it looks different for every run.

Sure, one could argue that there’s a context switch between println and the actual acquisition, but that would be an outline, not the norm as we see it in the result.

Conclusion

There seems to be some kind of order preservation, but it’s not 100% reliable. If, for your use case, the order is crucial, I suggest doing a custom queue implementation.

You might also like