scalalockingdeadlockreentrantlockreentrantreadwritelock

Reentrant locks within monads in Scala


A colleague of mine stated the following, about using a Java ReentrantReadWriteLock in some Scala code:

Acquiring the lock here is risky. It's "reentrant", but that internally depends on the thread context. F may run different stages of the same computation in different threads. You can easily cause a deadlock.

F here refers to some effectful monad.

Basically what I'm trying to do is to acquire the same reentrant lock twice, within the same monad.

Could somebody clarify why this could be a problem?

The code is split into two files. The outermost one:

val lock: Resource[F, Unit] = for {
  // some other resource
  _ <- store.writeLock
} yield ()

lock.use { _ => 
  for {
    // stuff
    _ <- EitherT(store.doSomething())
    // other stuff
  } yield () 
}

Then, in the store:

import java.util.concurrent.locks.{Lock, ReentrantReadWriteLock}
import cats.effect.{Resource, Sync}

private def lockAsResource[F[_]](lock: Lock)(implicit F: Sync[F]): Resource[F, Unit] =
  Resource.make {
    F.delay(lock.lock())
  } { _ =>
    F.delay(lock.unlock())
  }

private val lock = new ReentrantReadWriteLock
val writeLock: Resource[F, Unit] = lockAsResource(lock.writeLock())

def doSomething(): F[Either[Throwable, Unit]] = writeLock.use { _ =>
  // etc etc
}

The writeLock in the two pieces of code is the same, and it's a cats.effect.Resource[F, Unit] wrapping a ReentrantReadWriteLock's writeLock. There are some reasons why I was writing the code this way, so I wouldn't want to dig into that. I would just like to understand why (according to my colleague, at least), this could potentially break stuff.

Also, I'd like to know if there is some alternative in Scala that would allow something like this without the risk for deadlocks.


Solution

  • IIUC your question:

    You expect that for each interaction with the Resource lock.lock and lock.unlock actions happen in the same thread.

    1) There is no guarantee at all since you are using arbitrary effect F here. It's possible to write an implementation of F that executes every action in a new thread.

    2) Even if we assume that F is IO then the body of doSomething someone could do IO.shift. So the next actions including unlock would happen in another thread. Probably it's not possible with the current signature of doSomething but you get the idea.

    Also, I'd like to know if there is some alternative in Scala that would allow something like this without the risk for deadlocks.

    You can take a look at scalaz zio STM.