Feedback on Structured Concurrency (JEP 525, 6th Preview)
Remi Forax
forax at univ-mlv.fr
Sun Oct 12 13:43:00 UTC 2025
> From: "Jige Yu" <yujige at gmail.com>
> To: "loom-dev" <loom-dev at openjdk.org>
> Sent: Sunday, October 12, 2025 7:32:33 AM
> Subject: Feedback on Structured Concurrency (JEP 525, 6th Preview)
> Hi Project Loom.
> First and foremost, I want to express my gratitude for the effort that has gone
> into structured concurrency. API design in this space is notoriously difficult,
> and this feedback is offered with the greatest respect for the team's work and
> in the spirit of collaborative refinement.
> My perspective is that of a developer looking to use Structured Concurrency for
> common, IO-intensive fan-out operations. My focus is to replace everyday async
> callback hell, or reactive chains with something simpler and more readable.
> It will lack depth in the highly specialized concurrent programming area. And I
> acknowledge this viewpoint may bias my feedback.
[...]
> Suggestions for a Simpler Model
> My preference is that the API for the most common use cases should be more
> declarative and functional .
> 1.
> Simplify the "Gather All" Pattern: The primary "fan-out and gather" use case
> could be captured in a simple, high-level construct. An average user shouldn't
> need to learn the wide API surface of StructuredTaskScope + Joiner + the
> lifecycles. For example:
> Java
> // Ideal API for the 80% use case Robot robot = Concurrently.call(
> () -> fetchArm(),
> () -> fetchLeg(),
> (arm, leg) -> new Robot(arm, leg)
> );
I'm curious how you want to type that API, does it work only for two tasks, do you have an overload for each arity (2 tasks, 3 tasks, etc).
And how exceptions are supposed to work given that the type system of Java is not able to merge type variable representing exceptions correctly.
> 1.
> Separate Race Semantics into Composable Operations: The "race" pattern feels
> like a distinct use case that could be implemented more naturally using
> composable, functional APIs like Stream gatherers, rather than requiring a
> specialized API at all. For example, if mapConcurrent() fully embraced
> structured concurrency, guaranteeing fail-fast and happens-before, a
> recoverable race could be written explicitly:
> Java
> // Pseudo-code for a recoverable race using a stream gatherer <T> T race
> (Collection<Callable<T>> tasks, int maxConcurrency) { var exceptions = new
> ConcurrentLinkedQueue<RpcException>(); return tasks.stream()
> .gather(mapConcurrent(maxConcurrency, task -> { try { return task.call();
> } catch (RpcException e) { if (isRecoverable(e)) { // Selectively recover
> exceptions.add(e); return null ; // Suppress and continue } throw new
> RuntimeException(e); // Fail fast on non-recoverable }
> }))
> .filter(Objects::nonNull)
> .findFirst() // Short-circuiting and cancellation .orElseThrow(() -> new
> AggregateException(exceptions));
> }
> While this is slightly more verbose than the JEP example, it's familiar Stream
> semantics that people have already learned, and it offers explicit control over
> which exceptions are recoverable versus fatal. The boilerplate for exception
> aggregation could easily be wrapped in a helper method.
Several points :
- I believe the current STS API has no way to deal with if the exception is recoverable or not because it's far easier to do that at the end of the callable.
Your example becomes :
sts.fork(() -> {
try {
taskCall();
} catch(RPCException e) {
...
}
});
- You do not want to post the result/exception of a task into a concurrent data structure, i think the idea of the STS API in this case is to fork all the tasks and then take a look to all the subtasks.
I believe it's more efficient because there is no CAS to be done if the main thread take a look to the subtasks afterward than if the joiner tries to maintain a concurrent data structure.
> 1.
> Reserve Complexity for Complex Cases: The low-level StructuredTaskScope and its
> policy mechanism are powerful tools. However, they should be positioned as the
> "expert-level" API for building custom frameworks. Or perhaps just keep them in
> the traditional ExecutorService API. The everyday developer experience should
> be centered around simpler, declarative constructs that cover the most frequent
> needs.
For me, that's why you have an open Joiner interface for expert and already available Joiner (like all.../any...) that are more for everyday developers.
regards,
Rémi
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251012/6cc2ff9b/attachment.htm>
More information about the loom-dev
mailing list