4

I recently came across some odd behavior on a machine where maping a function that returns Future[T] was executing sequentially. This same problem does not occur on other machines: work is interleaved as one would expect. I later discovered that this was likely because Scala was being a little too smart and choosing an ExecutionContext that matched the machine's resources: one core, one worker.

Here's some simple code that reproduces the problem:

import scala.concurrent._
import scala.concurrent.duration._

val ss = List("the", "quick", "brown", "fox",
              "jumped", "over", "the", "lazy", "dog")

def r(s: String) : Future[String] = future {
        for (i <- 1 to 10) yield {
                println(String.format("r(%s) waiting %s more seconds...",
                        s, (10 - i).toString))
                Thread.sleep(1000)

        }
        s.reverse
}

val f_revs = ss.map { r(_) }

println("Look ma, no blocking!")

val rev = f_revs.par.map { Await.result(_, Duration.Inf) }.mkString(" ")

println(rev)

Running this on the machine exhibiting the strange behavior produces sequential output like this:

Look ma, no blocking!
r(the) waiting 9 more seconds...
r(the) waiting 8 more seconds...
r(the) waiting 7 more seconds...

Providing a custom ExecutionContext:

val pool = Executors.newFixedThreadPool(1)
implicit val ec = ExecutionContext.fromExecutor(pool)

allows the threads to interleave on this machine. But I have a new problem now: the thread pool does not shutdown, causing the program to hang indefinitely. Apparently, this is the expected behavior for FixedThreadPools, and I can shut it down by putting pool.shutdown() somewhere.

Call me stubborn, but I don't want to have to tell the thread pool to shutdown. Is there a way to configure the pool to shutdown when all of the queues are empty (possibly after some delay) just like it works with the default pool? I've looked through the ExecutionContext documentation, but I'm not finding what I'm looking for.

Dan Barowy
  • 2,270
  • 24
  • 35

2 Answers2

2

Scala uses its own fork-join implementation which behaves differently from the Java ones, hence the different behavior between the default ExecutionContext and the one you created with Executors.

An easier way to do what you want would be to set the following System properties to configure the default ExecutionContext:

  1. scala.concurrent.context.minThreads to impose a minimum number of threads. Defaults to 1.
  2. scala.concurrent.context.numThreads to set the number of thread. Defaults to x1.
  3. scala.concurrent.context.maxThreads to impose a maximum number of threads. Defaults to x1.

Each of these can either be a number, or a number preceded by x, to indicate a multiple of the number of processors. To increase the number of threads, you have to change both numThreads and maxThreads. In you case, setting both to x2 should work.

wingedsubmariner
  • 13,350
  • 1
  • 27
  • 52
1

It looks like Java 7 has some additional ExecutorServices, in particular, a ForkJoinPool that does what I want (i.e., no need to shutdown() the pool).

Changing the pool to the following is sufficient to achieve what I want:

val pool = new java.util.concurrent.ForkJoinPool(5)

Java 8 apparently has even more services.

Dan Barowy
  • 2,270
  • 24
  • 35