Accommodate BlockContext for locking

I think it would be nice if Scala supported blocking {}-style operations with Locks.

For example:

val myLock: ReentrantLock

withLock(myLock) {
  ...
}

Where withLock is a Scala library function along the lines of:

def withLock[T](lock: Lock)(thunk: () => T): T = {
  BlockContext.current.lock(lock)
  try {
    thunk
  } finally {
    lock.unlock()
  }
}

BlockContext.lock() is a new function which is lock.lock() in the default BlockingContext, and uses ManagedLocker from the Javadoc of ManagedBlocker in FP-related BlockContext in ExecutionContextImpl.

Hi @leventov,

I’m not sure it is worth to grow the API surface area for what is a couple of rather straightforward lines of code:

def withLock[T](lock: Lock)(thunk: => T): T = blocking {
  lock.lock()
  try thunk finally lock.unlock()
}

The code above should do exactly what you want, and is customizable w.r.t. behavior by it being in scope of an installed BlockContext—for instance, when running on a ExecutionContext.global thread it would already use ManagedBlocker under the hood, since that is what that EC installs as BlockContext.

1 Like

It’s not exactly the same. If it was, what would be the point of ManagedLocker example in ManagedBlocker's Javadoc. The difference is that with ManagedLocker, we can avoid growing the FJP’s pool by one thread if the lock is free (tryLock() returns true).

Another difference is that scope of thread compensation is much smaller: just the lock operation instead of the whole block with the execution of thunk, so FJP will be able to scale down the compensation thread sooner. I think it should be possible to conceive an example of poorly written async code with blocking where wrapping with blocking {} will lead to allocation of a huge number of compensation threads (that, in turn, may lead to OOM and crashes), whereas the code with withLock() and ManagedLocker under the hood may suffer relatively small performance degradation.

def withLock[T](lock: Lock)(thunk: => T): T = 
  if (lock.tryLock()) {
    try thunk finally lock.unlock()
  } else blocking {
    lock.lock()
    try thunk finally lock.unlock()
  }
2 Likes

A better version:

def withLock[T](lock: Lock)(thunk: => T): T = {
  if (!lock.tryLock()) {
    blocking { lock.lock() }
  }
  try thunk finally lock.unlock()
}

Though still not precisely identical to ManagedLocker (in the guts of JFP, tryCompensate() may fail and the lock may become available by that time, which ManagedLocker takes advantage of but the code above doesn’t) it’s close enough.

Thanks!