Hello, we catch some really weird error: ```2025-...
# coroutines
a
Hello, we catch some really weird error:
Copy code
2025-08-25 22:44:15.278 [DefaultDispatcher-worker-10] WARN  a.r.backend.api.KtorApp - null
java.lang.IllegalMonitorStateException: null
	at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$Sync.tryRelease(Unknown Source)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
	at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.unlock(Unknown Source)
	at ai.retable.backend.dataflow.processing.design.App.veryImportant(App.kt:619)
Code that cause error:
Copy code
val lock = ReentrantReadWriteLock() // java.util.concurrent.locks

suspend fun veryImportant() {
  lock.write {
      withTimeout(TIMEOUT) {
        // do some suspend work
      }
  }
}
Java code that cause an error:
Copy code
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

protected final boolean isHeldExclusively() {
 // While we must in general read state before owner,
 // we don't need to do so to check if current thread is owner
 return getExclusiveOwnerThread() == Thread.currentThread();
}
So, does it mean I can't use
java.util.concurrent.locks.ReentrantReadWriteLock
with Kotlin coroutines because the thread can change?
r
if something inside
.write
suspends, and the coroutine dispatcher has more than one thread, then yes the thread can change There's nothing really coroutine related in the code above though, no scope, no suspend, no evidence that coroutines are used at all?
ah aside from the DefaultDispatcher in the log, which is a multiple thread backed dispatcher
a
oops, forgot about that part. Yes it has a suspend here.
Copy code
lock.write{
    withTimeout(TIMEOUT) {
        // do some suspend work
    }
}
changed the question
r
generally you want to use suspending "locking" in coroutines e.g. Mutex & Semaphore
a
It doesn't have ReadWrite locking 😞
r
Copy code
/** A non-reentrant suspending implementation of a Read-Write lock. */
class ReadWriteLock {
  private var readerCount = 0
  private val readerMutex = Mutex()
  private val writerMutex = Mutex()

  suspend fun <R> withWriteLock(block: suspend () -> R): R = writerMutex.withLock(null) { block() }

  suspend fun <R> withReadLock(block: suspend () -> R): R =
      withContext(NonCancellable) {
        readLock()
        try {
          block()
        } finally {
          readUnlock()
        }
      }

  suspend fun readLock() =
      withContext(NonCancellable) {
        readerMutex.withLock {
          readerCount++
          if (readerCount == 1) writeLock()
        }
      }

  suspend fun readUnlock() =
      withContext(NonCancellable) {
        readerMutex.withLock {
          readerCount--
          if (readerCount == 0) writeUnlock()
        }
      }

  suspend fun writeLock() = writerMutex.lock()

  fun writeUnlock() = writerMutex.unlock()
}
(double check that, but it's worked for me)
a
Yep, I could have written it myself, but I wanted to avoid that 🙂 Reinventing the wheel
because it is a part of kotlin
Copy code
/**
 * Executes the given [action] under the write lock of this lock.
 *
 * The function does upgrade from read to write lock if needed, but this upgrade is not atomic
 * as such upgrade is not supported by [ReentrantReadWriteLock].
 * In order to do such upgrade this function first releases all read locks held by this thread,
 * then acquires write lock, and after releasing it acquires read locks back again.
 *
 * Therefore if the [action] inside write lock has been initiated by checking some condition,
 * the condition must be rechecked inside the [action] to avoid possible races.
 *
 * @return the return value of the action.
 */
@kotlin.internal.InlineOnly
public inline fun <T> ReentrantReadWriteLock.write(action: () -> T): T {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    val rl = readLock()

    val readCount = if (writeHoldCount == 0) readHoldCount else 0
    repeat(readCount) { rl.unlock() }

    val wl = writeLock()
    wl.lock()
    try {
        return action()
    } finally {
        repeat(readCount) { rl.lock() }
        wl.unlock()
    }
}
Copy code
package kotlin.concurrent.Locks.kt
r
Yes but as you've discovered the Java concurrent operators are designed for blocking, non-coroutine use Also given this your internal
suspend
code is no longer really suspending, as you are blocking the coroutine thread with the lock
a
aha, that is the answer. So it is a non-documented bug
r
a
Yep, this does not mention this kind of behaviour at all.
r
anyway anywhere there is a
suspend
it is essentially registering a callback and can resume on a different thread to the one it started on so best to think of it in that regard
a
Yep, I will pay more attention.
@ross_a For a read lock, you need a semaphore when implementing a real
ReadWriteLock
because multiple simultaneous reads are allowed, but only one write can occur at a time. Using a mutex on the readLock achieves nothing, isn't it?
r
I don't really understand the question, Mutex is roughly Semaphore(1) but the lock is only acquired to increase/decrease the number of readers and lock/unlock the write lock, it's doesn't stop concurrent reads, and is not bound to a maximum number of readers. This (noddy) implementation is prone to writer starvation but that's the main issue I'm aware of.
a
Oh, I get it now, you are right
Thanks, will try to move to that code
k
Might I suggest that you chime in on this issue? https://github.com/Kotlin/kotlinx.coroutines/issues/94
People have wanted a Read-Write mutex for a long time and we should continue to advocate to the maintainers for one
👌 1
r
looking at that issue, there appear to be more complete solutions than mine on there
👍 1
k
Yeah, I don't believe a RWMutex is easy to implement.
I'm a bit suspect of your
NonCancellable
usages in the above, too.
r
Yeah mine was chucked together quickly for a small non critical use case
👍 1
k
As long as it gets the job done 🫡