Search code examples
scalalockingsynchronizedcurryingpartialfunction

Scala Partial Function Application Semantics + locking with synchronized


Based on my previous question on locks based on value-equality rather than lock-equality, I came up with the following implementation:

/**
  * An util that provides synchronization using value equality rather than referential equality
  * It is guaranteed that if two objects are value-equal, their corresponding blocks are invoked mutually exclusively.
  * But the converse may not be true i.e. if two objects are not value-equal, they may be invoked exclusively too
  * Note: Typically, no need to create instances of this class. The default instance in the companion object can be safely reused
  *
  * @param size There is a 1/size probability that two invocations that could be invoked concurrently is not invoked concurrently
  *
  * Example usage:
  *   import EquivalenceLock.{defaultInstance => lock}
  *   def run(person: Person) = lock(person) { .... }
  */
class EquivalenceLock(val size: Int) {
  private[this] val locks = IndexedSeq.fill(size)(new Object())
  def apply[U](lock: Any)(f: => U) = locks(lock.hashCode().abs % size).synchronized(f)
}

object EquivalenceLock {
  implicit val defaultInstance = new EquivalenceLock(1 << 10)
}

I wrote some tests to verify that my lock functions as expected:

import EquivalenceLock.{defaultInstance => lock}

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.collection.mutable

val journal = mutable.ArrayBuffer.empty[String]

def log(msg: String) = journal.synchronized {
  println(msg)
  journal += msg
}

def test(id: String, napTime: Int) = Future {
  lock(id) {
    log(s"Entering $id=$napTime")
    Thread.sleep(napTime * 1000L)
    log(s"Exiting $id=$napTime")
  }
}

test("foo", 5)
test("foo", 2)

Thread.sleep(20 * 1000L)

val validAnswers = Set(
  Seq("Entering foo=5", "Exiting foo=5", "Entering foo=2", "Exiting foo=2"),
  Seq("Entering foo=2", "Exiting foo=2", "Entering foo=5", "Exiting foo=5")
)

println(s"Final state = $journal")
assert(validAnswers(journal))

The above tests works as expected (tested over millions of runs). But, when I change the following line:

def apply[U](lock: Any)(f: => U) = locks(lock.hashCode().abs % size).synchronized(f)

to this:

def apply[U](lock: Any) = locks(lock.hashCode().abs % size).synchronized _

the tests fail.

Expected:

Entering foo=5
Exiting foo=5
Entering foo=2
Exiting foo=2

OR

Entering foo=2
Exiting foo=2
Entering foo=5
Exiting foo=5

Actual:

Entering foo=5
Entering foo=2
Exiting foo=2
Exiting foo=5

The above two pieces of code should be the same and yet the tests (i.e. the lock(id) always enters concurrently for the same id) for the second flavor (the one with partial application) of code. Why?


Solution

  • By default function parameters are evaluated eagerly. So

    def apply[U](lock: Any) = locks(lock.hashCode().abs % size).synchronized _
    

    is equivalent to

    def apply[U](lock: Any)(f: U) = locks(lock.hashCode().abs % size).synchronized(f)
    

    in this case f is evaluated before the synchronized block.