0

Kotlin document states that Mutex is for synchronizing co-routines, not System-level threads:

The key difference is that Mutex.lock() is a suspending function. It does NOT block a thread. - kotlin doc

However, when I tried to reproduce the behavior of Mutex not synchronizing system-level threads, I found that Mutex appears to block system-level threads. I am not following WHY it is appearing to synchronize system-level threads.

Code showing Mutex appearing to block system-level threads:

The following is the code example:

package com.glassthought.sandbox

import gt.sandbox.util.output.Out
import com.glassthought.sandbox.util.out.impl.OutSettings
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.concurrent.thread

val mutex = Mutex()

var counter = 0

suspend fun test() {
  mutex.withLock {
    counter = counter + 1
  }
}

val out = Out.standard(OutSettings(printCoroutineName = false))
val TIMES_TO_REPEAT = 100000

suspend fun main(args: Array<String>) {

  out.info("Starting on main thread")

  val t1 = thread {
    runBlocking {
      incrementInALoop("thread-1")
    }
  }

  val t2 = thread {
    runBlocking {
      incrementInALoop("thread-2")
    }
  }

  t1.join()
  t2.join()

  printResults()
}

private suspend fun incrementInALoop(threadName: String) {
  out.info("Starting execution on $threadName")
  repeat(TIMES_TO_REPEAT) {
    test()
  }
  out.info("Finished execution on $threadName")
}

private suspend fun printResults() {
  out.info("Counter : $counter")
  val expected = TIMES_TO_REPEAT * 2
  out.info("Expected: $expected")
  if (expected == counter) {
    out.printGreen("All accounted!")
    out.println("")
  } else {
    out.printRed("NOT all accounted!")
    out.println("")
  }
}

The output shows:

[elapsed:   18ms][🥇/tname:main/tid:1] Starting on main thread
[elapsed:   46ms][⓶/tname:Thread-0/tid:21] Starting execution on thread-1
[elapsed:   46ms][⓷/tname:Thread-1/tid:22] Starting execution on thread-2
[elapsed:  250ms][⓶/tname:Thread-0/tid:21] Finished execution on thread-1
[elapsed:  250ms][⓷/tname:Thread-1/tid:22] Finished execution on thread-2
[elapsed:  255ms][🥇/tname:main/tid:1] Counter : 200000
[elapsed:  255ms][🥇/tname:main/tid:1] Expected: 200000
All accounted!

[🥇/tname:main/tid:1] part of output is thread name. We can see that the two non main threads: [[⓶/tname:Thread-0/tid:21], [⓷/tname:Thread-1/tid:22]] start at about the same time and finish at about the same time, showing us that they are running in parallel.

I am expecting the counter to be off from what it is expected to be. However, the counter is as expected. Appearing as co-routine Mutex running as synchronizing mechanism for system level threads.

Without mutex usage:

IF we remove the mutex usage in

suspend fun test() {
  mutex.withLock {
    counter = counter + 1
  }
}

Function to be:

suspend fun test() {
 //  mutex.withLock {
    counter = counter + 1
 // }
}

We will see that the counter is off from what it is expected to be:

[elapsed:   18ms][🥇/tname:main/tid:1] Starting on main thread
[elapsed:   43ms][⓶/tname:Thread-1/tid:22] Starting execution on thread-2
[elapsed:   43ms][⓷/tname:Thread-0/tid:21] Starting execution on thread-1
[elapsed:   51ms][⓷/tname:Thread-0/tid:21] Finished execution on thread-1
[elapsed:   51ms][⓶/tname:Thread-1/tid:22] Finished execution on thread-2
[elapsed:   53ms][🥇/tname:main/tid:1] Counter : 144993
[elapsed:   53ms][🥇/tname:main/tid:1] Expected: 200000
NOT all accounted!

Back to question

Why does kotlinx.coroutines.sync.Mutex appear to block system level threads?

3
  • I'm not entirely sure why you expected an "unexpected" result. Mutex doesn't block the thread, but it "blocks" the coroutine, so only one coroutine could enter withLock at a time. By saying Mutex doesn't block the thread we mean that while one coroutine waits on entering the withLock block, the same thread could execute another coroutine. Commented Dec 4, 2024 at 17:19
  • @broot Thanks for your reply. I think it's my mental modeling that needs to be corrected. I had it in my mind that co-routines are a sort of sub-threads, as threads are to a process. Meaning each thread having multiple co-routines under it. While, what you are implying (if I understand correctly) is that I should think of coroutines as more of a global pool of coroutines, where one coroutine can be executed on different threads. And this Mutex would work across this global pool of coroutines and therefore be able to perform synchronization across threads. Commented Dec 4, 2024 at 17:43
  • Yes, coroutines are dispatched on threads for execution. You can read more about that on official documentation about coroutine context and dispatchers. In your example, you run coroutines using a runBlocking block, which effectively create a single thread dispatcher using the calling thread. runBlocking will block the calling thread waiting the coroutine to finish, its usage should be avoided if possible. You can try tod launch both coroutines in the same runBlocking thread to see how it affects output. Commented Dec 5, 2024 at 9:32

1 Answer 1

0

It's not the thread directly that is affected by the mutex, it is the coroutine.

In each thread you start a new coroutine with runBlocking which accesses the mutex. When locked, the current coroutine is suspended, freeing (not blocking!) the thread to potentially do something else.

So while it is obvious that the first example counts correctly (otherwise the whole purpose of the mutex would be defeated), this doesn't indicate that the mutex blocks the threads. Both threads still wait a lot for the other thread to perform its calculation, but the threads are not blocked.

Sign up to request clarification or add additional context in comments.

3 Comments

Can we say that if we use a kotlinx.coroutines.sync.Mutex, there is a guarantee that within withLock the code will be synchronized and NO 2 threads OR co-routines would be able to execute the contents of the block within withLock in parallel?
Yes. Since mutexes cannot exist without coroutines, and coroutines cannot exist without threads by association mutexes also protect across threads, not just coroutines.
One note: mutex protects only the entry point. While a coroutine is running inside it, it can spawn more coroutines and they can use multiple threads. Mutex doesn't limit the concurrency/parallelism inside the withLock block, it works on the entry/exit boundary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.