[External] : Re: The Great Concurrency Smackdown: ZIO versus JDK by John A. De Goes
Ron Pressler
ron.pressler at oracle.com
Wed Mar 15 09:53:17 UTC 2023
On 15 Mar 2023, at 08:48, Alex Otenko <oleksandr.otenko at gmail.com<mailto:oleksandr.otenko at gmail.com>> wrote:
Re: race and leaks
I think he is referring to the fact that even if you have a timeout, the tasks that haven't completed keep running. Maybe he is mistaken about it, but that's how I understand it, and as far as I can see the current blocking APIs don't solve this problem.
He's mistaken. When the STS closes, all threads must have terminated. It’s not the `join` method that keeps waiting, but the `close` method.
The claim is that various effects, of which race and termination are only two examples, can be captured by the type system and allows for better control of such effects.
Maybe someone claims that, but the speaker in this talk claims the opposite: “The value of effect systems is NOT tracking. It’s transforming and combining statements-as-values”. Someone who deals with effects should know that threads allow all the same transformations and combinations because a blocked thread represents the same value that effect systems turn their “statements” into, and offers the same composition.
This dig, of course, is not about Loom at all. This is about the old school programming languages where sequence of statements has so many effects, but no way to control them.
Typed effect systems are about 30 years old — older than Java. They just feel fresh because they never manage to gain wide adoption since they’ve yet to demonstrate a significant practical utility for controlling side effects.
That’s not to say that we’ve learned nothing useful from effect systems, it’s just that the particular purpose of controlling side effects has not yet borne fruit. When it does you’ll start seeing mainstream languages adopt the idea.
The “dig” here is nothing more than, “I enjoy the aesthetics of writing code in the functional style more than in the imperative style,” and *that* is a very valid thing to say! But as is often the case, people try to give their preferences some universal truth, and quite often — as happens in this talk — it just leads to making many incorrect statements.
Alex
On Tue, 14 Mar 2023, 17:27 Ron Pressler, <ron.pressler at oracle.com<mailto:ron.pressler at oracle.com>> wrote:
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://urldefense.com/v3/__https://www.youtube.com/watch?v=9I2xoQVzrhs__;!!ACWV5N9M2RV99hQ!JLnxciWEJ_q4khrnpuHbY2HXCkyPoNt_mUGZ1eRF5fQ_iEEAGQL1sjYodKT85O0ReAhMr6PXi0PzxhIjYFCG_-XOMPR4$>
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/20230315/37d35870/attachment-0001.htm>
More information about the loom-dev
mailing list