The Great Concurrency Smackdown: ZIO versus JDK by John A. De Goes

Ron Pressler ron.pressler at oracle.com
Tue Mar 14 17:26:57 UTC 2023


Hi Eric.

I watched the talk and was able to find just one valid point in it. The rest fall
into three categories: 1. lack of familiarity with Java's features 2. lack of familiarity
with imperative composition, and 3. a misunderstanding of what it is that virtual
threads (or even the JDK in general) let you do. The points in the first two categories
are, I think, erroneous; the ones in the third category are mostly irrelevant.

The first category first:

1. That InheritableThreadLocals have less-than-ideal overhead is one of the
reasons we've added ScopedValues (JEP 429), which are designed to interoperate
well with structured concurrency.

2. Interruption is as reliable as anything that can be offered in either Java or
Scala. The mechanism could be made more friendly, but I don't think that's what
he's talking about. It may not be as reliable as what Erlang can offer thanks
to its special restrictions on the language.

3. StructuredTaskScope does not leak threads. Closing the scope waits for all
threads to terminate.

4. StructureTaskScope's error handling isn't lossy. I think that's misunderstood
partly because he hasn't closely looked at the API and partly because he missed
the significance to observability of every fork running in its thread; I hope
that significance will become clearer in JDK 21. In short, because every task
is its own thread, it can report exceptions to interested observers with an
observable context; as a result, the parent only needs to be made aware of
exceptions that directly affect it, but it need not offer any additional
assistance in observing the forks. Nothing here is lossy -- all exceptions are
reported and the scope is always made aware of any fork exception that could
impact it.

5. The point about threads not being “type-safe” misses the fact that in Java
threads are not used via the Thread API directly, but through ExecutorService
APIT (or the new StructuredTaskScope). Something could be said about checked
exceptions, I guess, but that’s a completely separate topic, a hard one, and not
one where the asynchronous-functional approach particularly shines, either.

6. The TwR construct in Java does not require nesting in the form shown in the
talk (the speaker seems unfamiliar with Java's TwR syntax). However, there is a
good point here and that is that AutoCloseables are not composable, which
requires work to support dynamic resources. We’re working on designing a more
capable, and composable, interface than AutoCloseable to work in TwR, but it’s
not super-high priority at this time. That was the only valid point I managed
to find in the entire talk.

The second category is exemplified by the speaker's mistake about the
composability of the `race` method he presents. This is how obvious the
composition is:

    <T> T withTimeout(Instant deadline, Callable<T> task) throws
    TimeoutException, InterruptedException, ExecutionException { try (var s =
    new StructuredTaskScope.ShutdownOnSuccess()) { s.fork(task); return
    s.joinUntil(deadline).result(); } }

It can be composed with `race`, or any operation, anywhere in the hierarchy.
I.e., instead of `race(tasks, deadline)` you can write `withTimeout
(deadline, race(tasks))`.

He tries to explain the "composition deficiency" he believes he detects by
pointing out that statement composition is not a composition of values. But
someone who thinks about functional effects should immediately see the
theoretical error here because statement composition is not functional "Unit"
composition, but rather monadic composition; and the monad here is reified
where (in other words, where is the `bind`)? In the thread. When an operation
blocks it effectively returns a monadic value to the scheduler, which exposes
the means by which the operation be transformed and composed with, say, a
timeout. That's why a functional effects person should immediately suspect that
if they can't see how the transformation can be applied to Java code they must
be missing something. Examination of the code for `race` itself would quickly
show how this transformation can be factored out of `race` and abstracted.

The final category is exemplified by the truly bizarre point about
retries/backoff and loops. That virtual threads allow implementing retries
using loops doesn't mean that that's what user code looks like. I would think
this is almost too obvious to state, and yet what's shown in the talk is not
how programming works in virtually any high-level language or any style.
Rather, that retries can be efficiently implemented using loops simply means
that by selecting the right building blocks we managed to avoid adding retries
as a primitive and can allow such a general abstraction to be written in a way
that is 1. composable with any I/O library and 2. interacts well with the
platform's observability. That's what a win is (at least for a language).

Boasting of a library where retries are primitives, they cannot compose with
other libraries, and they do not interact well with observability is just
puzzling in this context. Thanks to virtual threads, libraries can offer a
truly composable and observable retry construct (because they can be
implemented with something as simple as a loop). He further boasts that the
implementation of a synchronous `retryWithExponentialBackoff` library routine
won't fit on a slide, even though it would still be shorter than an
asynchronous implementation...

Having new code specificially written for one framework compose with other code
written for that framework is a very low bar to clear, and it can only work if
you manage to attract the ecosystem over to program with your new framework;
virtual threads were added to Java only after a lot of people complained that
that approach doesn't work -- it doesn't compose with the code they already
have and requires too much rewriting, it doesn't work with their tools, and too
many of them simply don't enjoy it. What virtual threads do, on the other hand,
is allow writing concurrent code that composes with virtually any other simple
synchronous code ever written and that will ever be written for the Java
platform, and with the same observability and debuggability Java developers
expect. We're able to do that not because we're smarter than library authors,
but because these things require deep platform support. I think it won't be too
long before we know whether or not our approach works.

The JDK — a language and runtime platform — hasn't ever intended to eliminate
the need for third-party libraries, nor does it now. Quite the opposite: we try
to include as little as we can get away with. The JDK's goal is to provide the
right building blocks. None of the features produced by Loom to date -- virtual
threads, structured concurrency, scoped values -- could have been implemented
by libraries, as they all required changes to the platform. They are the
building blocks on top of which third party libraries -- including those that
may provide "Refs" -- can be written in a way that is more composable and
observable than was ever before possible on the Java platform. Conversely, I
don't believe Loom has contributed any feature that could have been implemented
in a third-party library; we've so far focused only on the primitives.

Having said all that, it's important to remember that he's talking about a
library that's aimed at programmers who like different things than Java
programmers so much so that they use a different language, and, I'm sure at
least some of the high-level libraries will continue to cater to those who
aesthetically prefer the functional style in whatever language they enjoy
programming for the Java platform.

The important question that wasn't asked in the talk at all is, "how should
high-level concurrency libraries work on top of the JDK now that we have
virtual threads?" I think that one big difference is that thanks to the
improved composability, high-level functionality that used to be offered by
monolithic frameworks could now be offered, a la carte, by more focused
libraries.

As to your question, the for-yield construct is just syntax for monadic
composition that's already built into Java's imperative composition, but as for
`foreachPar`, I believe we can do even better. We're already working on a way
that will lead to a structured concurrency construct that works with streams or
something similar to it. JEP 428 explicitly states that StructuredTaskScope may
not be the only structured concurrency construct we'll introduce.

-- Ron

On 10 Mar 2023, at 22:57, Eric Kolotyluk <eric at kolotyluk.net<mailto:eric at kolotyluk.net>> wrote:


The Great Concurrency Smackdown: ZIO versus JDK by John A. De Goes<https://www.youtube.com/watch?v=9I2xoQVzrhs>

I would recommend this presentation as interesting, worth watching.

However, I found a lot of problems with the arguments, and while John De Goes knows a few things about Loom, his knowledge is incomplete and out of date. On the other had, he does make a few interesting claims about Zio that might be nice features in Loom some day. In particular, the for-yield structure in Scala and Zio is a very powerful structure, although can be quite cryptic to fathom, especially by the most clever in the Scala community.

I will withhold my other insights and questions until people respond to this, showing interest in further discussion.

Cheers, Eric

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20230314/cbb3fb95/attachment-0001.htm>


More information about the loom-dev mailing list