[concurrency-interest] CompletableFuture in Java 8

Doug Lea dl at cs.oswego.edu
Mon Dec 15 10:42:46 EST 2014


On 12/13/2014 03:05 PM, √iktor Ҡlang wrote:
> On Sat, Dec 13, 2014 at 4:17 PM, Doug Lea <dl at cs.oswego.edu
> <mailto:dl at cs.oswego.edu>> wrote: It would be nice to simplify mixed usages
> across the various styles discussed on this and related threads, but this one
> is relatively straightforward even now:
>
> CompletableFuture.supplyAsync(__() -> data.parallelStream().reduce(.__..)).
> thenApply(...);
>
> Isn't the only reason why this works that they are both running on the
> commonPool?

Well, it always "works" even if you use the non-common-pool
parallel stream idiom, or use thenApplyAsync (vs thenApply)
with some different Executor. Some of the combinations may
transiently create and/or block some threads for the sake of
join-dependencies and hand-offs. It is worth some exploration
to streamline, but these are the kinds of cases in which
occasional needs to add threads, blocking, or queuing are not
usually major concerns, so long as there are no positive-feedback
effects leading to unbounded growth.

The more challenging cases lie elsewhere. Here's some pretentious
pontification about the underlying classic framework design issues.

Concurrent, parallel, and/or distributed APIs tend to be
either "pull" or "push" oriented:

Pull: Consumers call functions (possibly calling others, possibly in
parallel) returning results that are somehow used to produce visible
effects.  These APIs tend to apply best when data sources for a
computation already exist. Fork/Join-style frameworks generalize the
idea of a function call to enable internal parallelism.

Push: Producers trigger sets of computations (possibly in turn
triggering others) when data become available. ultimately leading to
some visible effects.  Reactive completion-based frameworks generalize
the idea of interrupt-handlers etc to allow arbitrary async flows.

In Java, Scala, etc, fluent lambda-based APIs have been found to be
pretty good in helping to structure either kind of parallel
computation, allowing (we hope) more productive exploitation of
multicores with fewer errors than seen with ad-hoc uses of
threads. But mixtures across these can run into performance and
resource problems that we'd like to better address.

Pull-style frameworks can encounter unbounded stalls and resources
when function calls entail external communication via blocking IO. And
Push-style frameworks cannot readily take advantage of efficient
in-place parallel-dag scheduling and execution. (For example, you
wouldn't want to use CompletableFutures for most divide-and-conquer
parallel processing.)

At the core library/JVM level though, concurrency support for both is
roughly similar: Encapsulating as tasks held in various data
structures and scheduling/managing execution. So in principle, we can
support arbitrary combinations. We do some of this already.  The
masochist-only CountedCompleter class requires manual continuation
passing transforms of fork/join style processing that can co-exist
with reactive processing. It is used internally in j.u.Stream, as well
as mixed parallel/distributed frameworks including 0xdata H2O. But
even ignoring the hostility/unusability factor, this approach alone
does not provide a complete solution when used with user-supplied
lambdas that can arbitrarily misbehave.

Short of any grand unified scheme, one approach is to improve
support that encourages replacements of the common enemy of
both of these API styles -- blocking IO. Where near-term
"encouragement" might take several forms, including adaptors
and utilities for CompletionStages, j.u.Streams, and other APIs.

-Doug





More information about the Concurrency-interest mailing list