Mutex is non-reentrant

If the same co-routine that acquired the lock attempts to acquire it again, it will deadlock. This is because the lock is not reentrant.

package com.glassthought.sandbox

import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock


val out = Out.standard()

class AttemptToGrabLockFromSameCoroutine {
  private var value = 0
  private val mutex = Mutex() // Mutex for coroutine-safe access to 'value'

  // Suspend function to use coroutine-friendly synchronization with timeout
  suspend fun outerCall() {
    out.println("outerCall outside of mutex")

    // Attempt to acquire the lock with a timeout
    val success = withTimeoutOrNull(1000L) { // 1000ms timeout
      mutex.withLock {
        out.println("outerCall INSIDE of mutex")
        innerCall()
      }
    }

    if (success == null) {
      out.println("ERROR: outerCall could not acquire the lock within timeout")
    }
  }

  private suspend fun innerCall() {
    out.println("innerCall outside of mutex")

    // Attempt to acquire the lock with a timeout
    val success = withTimeoutOrNull(1000L) { // 1000ms timeout
      mutex.withLock {
        out.println("innerCall INSIDE of mutex")
        value++
      }
    }

    if (success == null) {
      out.println("ERROR: innerCall could not acquire the lock within timeout")
    }
  }

  fun getValue() = value
}

fun main() = runBlocking {
  val attemptToGrabLockFromSameCoroutine = AttemptToGrabLockFromSameCoroutine()

  // Launch coroutines instead of creating threads
  coroutineScope {
    val jobs = (1..2).map {
      launch { attemptToGrabLockFromSameCoroutine.outerCall() }
    }
    jobs.forEach { it.join() } // Wait for all coroutines to complete
  }

  println("Value: ${attemptToGrabLockFromSameCoroutine.getValue()}")
}

Command to reproduce:

gt.sandbox.checkout.commit fec6776d79e44c8ebe56 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run"

Recorded output of command:

> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :app:compileKotlin UP-TO-DATE
> Task :app:compileJava NO-SOURCE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE

> Task :app:run
[2024-11-17T01:11:38.506307Z][elapsed-since-start:   54ms][tname:main/tid:1] outerCall outside of mutex
[2024-11-17T01:11:38.530549Z][elapsed-since-start:   66ms][tname:main/tid:1] outerCall INSIDE of mutex
[2024-11-17T01:11:38.530880Z][elapsed-since-start:   66ms][tname:main/tid:1] innerCall outside of mutex
[2024-11-17T01:11:38.532629Z][elapsed-since-start:   68ms][tname:main/tid:1] outerCall outside of mutex
[2024-11-17T01:11:39.536510Z][elapsed-since-start: 1072ms][tname:main/tid:1] ERROR: innerCall could not acquire the lock within timeout
[2024-11-17T01:11:39.538559Z][elapsed-since-start: 1074ms][tname:main/tid:1] ERROR: outerCall could not acquire the lock within timeout
[2024-11-17T01:11:39.539809Z][elapsed-since-start: 1075ms][tname:main/tid:1] ERROR: outerCall could not acquire the lock within timeout
Value: 0

BUILD SUCCESSFUL in 1s
2 actionable tasks: 1 executed, 1 up-to-date

Backlinks