The below (contrived) code attempts to print a by-name String parameter within a future, and return when the printing is complete.
import scala.concurrent._
import concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
class PrintValueAndWait {
def printIt(param: => String): Unit = {
val printingComplete = future {
println(param); // why does this hang?
}
Await.result(printingComplete, Duration.Inf)
}
}
object Go {
val str = "Rabbits"
new PrintValueAndWait().printIt(str)
}
object RunMe extends App {
Go
}
However, when running RunMe
, it simply hangs while trying to evaluate param
. Changing printIt
to take in its parameter by-value makes the application return as expected. Alternatively, changing printIt
to simply print the value and return synchronously (in the same thread) seems to work fine also.
What's happening exactly here? Is this somehow related to the Go object not having been fully constructed yet, and so the str
field not being visible yet to the thread attempting to print it? Is hanging the expected behaviour here?
I've tested with Scala 2.10.3 on both Mac OS Mavericks and Windows 7, on Java 1.7.
Your code is deadlocking on the initialization of the Go
object. This is a known issue, see e.g. SI-7646 and this SO question
Objects in scala are lazily initialized and a lock is taken during this time to prevent two threads from racing to initialize the object. However, if two threads simultaneously try and initialize an object and one depends on the other to complete, there will be a circular dependency and a deadlock.
In this particular case, the initialization of the Go
object can only complete once new PrintValueAndWait().printIt(str)
has completed. However, when param
is a by name argument, essentially a code block gets passed in which is evaluated when it is used. In this case the str
argument in new PrintValueAndWait().printIt(str)
is shorthand for Go.str
, so when the thread the future runs on tries to evaluate param
it is essentially calling Go.str
. But since Go
hasn't completed initialization yet, it will try to initialize the Go
object too. The other thread initializing Go
has a lock on its initialization, so the future thread blocks. So the first thread is waiting on the future to complete before it finishes initializing, and the future thread is waiting for the first thread to finish initializing: deadlock.
In the by value case, the string value of str
is passed in directly, so the future thread doesn't try to initialize Go
and there is no deadlock.
Similarly, if you leave param
as by name, but change Go
as follows:
object Go {
val str = "Rabbits"
{
val s = str
new PrintValueAndWait().printIt(s)
}
}
it won't deadlock, since the already evaluated local string value s
is passed in, instead of Go.str
, so the future thread won't try and initialize Go
.