Reputation: 15539
I had a code that used an ExecutionContext (EC) build with akka (ActorSystem). This code is doing something quite peculiar: it uses an AkkaForkJoinPool with parallelism-max = 1
and execute something like:
implicit ec = // akka EC backed by AkkaForkJoinPool with parallelism=1
Future{ // (1)
// (2) get data from DB which uses a separate ExecutionContext for IO
val data: Future[Data] = getData()
// (3) use the data
data.map{ whatEver }
// etc ...
}
[Edit: I know, put like that this is strange to have the top Future (1). But in reality the code is not my own, it span several functions, and uses more complex operations such as several wrapped for-comprehension. So I won't change that]
Now I moved this code and replace the implicit ExecutionContext (EC) provided by akka by my own following the same rule: I use a (java) ForkJoinPool with parallelism = 1.
As a consequence, this code get stuck at the map (3). My understanding is that when the map (3) is called it requires a thread but the EC cannot provide one because its only available one is taken by the Future (1).
I am not clear how the ForkJoinPool is suppose to work. So my question is did I understand correctly, and:
I am using akka 2.3.15, scala 2.11.12 and java 8
Upvotes: 0
Views: 129
Reputation: 15539
Looking into akka's code, I think I found what it does. I am not completely sure but almost: akka ActorSystem create a Dispatchers
which create a MessageDispatcherConfigurator
which create a Dispatcher
that creates the ExecutorService (I pass on the class hierarchy of that). There are several possible implementations, but this is the most common I think, and this is what happens when using a ForkJoinPool.
Now, the Dispatcher extends BatchingExecutor
which is the one that can batch together internal task such as the map in the question (which requires a Thread to run) to the current Thread.
Once again, the code is too complex for me to be sure and I won't investigate more. But indeed akka EC can wrap internal map call to the parent thread which is not what happens with standard (i.e. java) ForkJoinPool.
I think this is a smart trick from akka, and not a typical implementation. The doc of BatchingExecutor says:
/**
* Mixin trait for an Executor
* which groups multiple nested `Runnable.run()` calls
* into a single Runnable passed to the original
* Executor. This can be a useful optimization
* because it bypasses the original context's task
* queue and keeps related (nested) code on a single
* thread which may improve CPU affinity. However,
* if tasks passed to the Executor are blocking
* or expensive, this optimization can prevent work-stealing
* and make performance worse. Also, some ExecutionContext
* may be fast enough natively that this optimization just
* adds overhead.
* The default ExecutionContext.global is already batching
* or fast enough not to benefit from it; while
* `fromExecutor` and `fromExecutorService` do NOT add
* this optimization since they don't know whether the underlying
* executor will benefit from it.
* A batching executor can create deadlocks if code does
* not use `scala.concurrent.blocking` when it should,
* because tasks created within other tasks will block
* on the outer task completing.
* This executor may run tasks in any order, including LIFO order.
* There are no ordering guarantees.
*
* WARNING: The underlying Executor's execute-method must not execute the submitted Runnable
* in the calling thread synchronously. It must enqueue/handoff the Runnable.
*/
Upvotes: 0
Reputation: 6124
Instead of wrapping everything in a future, use a for-comprehension on the result of the first future, since everything depends on it.
for {
data <- getData()
} yield data.map( whatEver )
or
getData().map { data =>
data.map { whatEver }
}
Upvotes: 1