https://kotlinlang.org logo
Title
d

David W

12/29/2021, 8:38 PM
I have an inline function that takes a
List<Lock>
and an anonymous function. It needs to lock all of the locks, execute the function, and then unlock all locks. The anonymous function should also allow non-local returns. I've been banging my head against this for hours, anyone have a solution?
inline fun <T> write(locks: List<ReentrantReadWriteLock>, action: () -> T): T
The obvious solution is to iterate through each lock and lock it, run the function, and then iterate through and unlock each one.
but I've been getting `IllegalMonitorStateException`s for that if the anonymous function throws an exception (which should be allowed to propagate)
j

Joffrey

12/29/2021, 8:41 PM
You should unlock the locks in a
finally
block around the the call to
action()
, so that it's done in both normal and exceptional completion
d

David W

12/29/2021, 8:43 PM
i did that
inline fun <T> write(lockContext: LockContext = IOLocks.defaultLock, action: () -> T): T {
        val finalLock = lockContext.locks.last()
        lockContext.locks.forEach { lock ->
            if (lock === finalLock) {
                val result = try {
                    lock.write {
                        Timber.tag(tag)
                            .d { "Write locked from ${timber.Timber.findClassName()} on ${getCurrentThreadName()}." }
                        flow.tryEmit(true)

                        try {
                            action()
                        } finally {
                            Timber.tag(tag)
                                .d { "Write unlocked from ${timber.Timber.findClassName()} on ${getCurrentThreadName()}." }
                            flow.tryEmit(false)
                        }
                    }
                } finally {
                    (lockContext.locks - finalLock).forEach { lockToUnlock ->
                        Timber.d { "Unlocking $lockToUnlock" }
                        val readCount = if (lockToUnlock.writeHoldCount == 0) lockToUnlock.readHoldCount else 0
                        repeat(readCount) { lockToUnlock.readLock().lock() }
                        lockToUnlock.writeLock().unlock()
                    }
                }
                return result
            } else {
                Timber.d { "Locking $lock" }
                val rl = lock.readLock()
                val readCount = if (lock.writeHoldCount == 0) lock.readHoldCount else 0
                repeat(readCount) { rl.unlock() }
                lock.writeLock().lock()
            }
        }

        // If no locks, run func and return result.
        return action.invoke()
    }
and yet
java.lang.IllegalMonitorStateException
	at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$Sync.tryRelease(ReentrantReadWriteLock.java:372)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1006)
	at java.base/java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.unlock(ReentrantReadWriteLock.java:1147)
My
action
func is changing threads (default dispatcher) between when it locks and unlocks a lock
and that's causing the exception
I'm not sure why it's changing threads
If this were run on
Dispatchers.Default
, could it change threads during execution?
suspend fun Path.awaitWrite(timeoutMillis: Long = 1000): Path {
    var delayAcc = 0

    while (!this@awaitWrite.isWritable() && delayAcc < timeoutMillis) {
        delayAcc += 10
        delay(timeMillis = 10)
    }

    if (this@awaitWrite.isWritable()) {
        return this@awaitWrite
    } else {
        throw RuntimeException("Timed out waiting for write access to $this.")
    }
}
yeah, that was it i think i wrapped the above in
runBlocking
and no more
IllegalMonitorStateException
j

Joffrey

12/29/2021, 9:48 PM
If this were run on 
Dispatchers.Default
, could it change threads during execution?
Yes, every suspension point (every call to a suspend function) acts as a dispatch point, and the suspend function can resume on any thread of the dispatcher.
e

ephemient

12/30/2021, 12:43 AM
if you are working with coroutines, you should use kotlinx.coroutines.sync.Mutex. ideally we'd have a nosuspend modifier so that any inline fun working with Java synchronization can avoid being inlined with a suspend lambda
d

David W

12/30/2021, 2:29 AM
Hm,
Mutex
isn't reentrant, though. Each piece of my code that touches files is responsible for using a lock, so there are bits that call each other, resulting in double calls to the same lock from the same thread, by design.
I'm also using this for file I/O, and there's no equivalent of
ReentrantReadWriteLock
for Mutex that i see
e

ephemient

12/30/2021, 2:52 AM
that design sounds incompatible with coroutines then :-\
there's an issue for an efficient ReadWriteMutex but if you're doing I/O then you can build your own simple version from counter+semaphore
however, re-entrant locking just doesn't work out
d

David W

12/30/2021, 3:31 AM
it does indeed seem to be incompatible
i'll need to be careful to avoid putting any code that can hop threads into my critical paths
i seem to be able to 'fix' my crash, when i attempted to unlock a lock from a different thread, by reducing the code within the lock
specifically, excluding the part that did networking and only including the part that is appending the buffered bytes to a file
a fragile solution, but for an unpaid hobby side project, good enough
n

Nick Allen

01/01/2022, 3:33 AM
Another option would be the reentrant lock here: https://elizarov.medium.com/phantom-of-the-coroutine-afc63b03a131
e

ephemient

01/02/2022, 4:28 AM
I don't think that's very helpful. it describes how you could build a recursive lock in kotlinx.coroutines (although I think that is still misguided, since by allowing any child job to freely acquire the lock, it's not protecting the locked object against concurrency), but it doesn't deal with Java's recursive locks, which must be unlocked on the same thread as they are locked on (and the coroutine dispatcher has no guarantee of resuming on the same thread that it was suspended on, as discussed earlier)