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.