JDK 19 Release Notes
jdk.java.net
jdk.java.net
Should be interesting to see what shakes out of all this. When you need structured concurrency, imo async and coroutines are nicer than what fork/join and what Java has so far.
Here's the example from the JEP:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // Join both forks
scope.throwIfFailed(); // ... and propagate errors
// Here, both forks have succeeded, so compose their results
return new Response(user.resultNow(), order.resultNow());
}
}I don't see the advantage to this over async/await style programming. I think you'll see very similar error cases with unobserved exceptions and such.
The advantage is that the method signature is indistinguishable from a blocking method. The subtle difference is that scopes close within a method where as async tasks can continue. I suppose you could see an even worse coloring where scopes and futures are used in half the methods instead of async.
I'm trying to wrap my head around how this would play out if you were attempting to write a GUI or something where the pattern is a single main thread. I suppose it would work just fine and look very similar to async style programming but with a lot more code one tab to the right.
Harmonious with the platform (jvm) which is based on threads, better stack-traces, accurate profiling info and simpler programming model (without adding async/await/suspend keyword everywhere) etc are some of the advantages.
As I understand, structured concurency mostly imply automatic cancellation and supervision that allows retry/restarts.
Does async/await gives you that?
If any async operation fails, at what level up the waiting chain do you want to cancel all other concurent tasks under it and retry the entire async flow?
So you need the APIs/syntax to have ways to define these scope of cancellation/retries.
As I understand, this is why it is called "structured", it refers to similar "structured" programming that introduced lexically scope restricted procedures, conditionals and loops.
Edit: Also, just want to point out that you shouldn't get mixed up between async/await and future/promise. Async/await often uses future/promise, but don't necessarily have too. But future/promise is an async API that existed before async/await, and supports threaded concurency just fine. You can use future/promise in Java with both threads or virtual threads. Using it with virtual threads won't color functions because at any time you can block on a future/promise and extract the value, so it's possible to go from a future/promise returning function to one that doesn't return a future/promise.
If 4 tasks are started asynchronously and one of them throws? What do you want to happen to the other 3?
In structured concurency you could say, if any of them fails then cancel all others, and retry them all.
Or for example, if one task is waiting on another and that first task throws an exception, you might want to say, ok, retry the first and re-schedule the other one to wait on that retried task.
These are all things you can handle yourself as well, but structured concurency is just trying to make that less effort and automatic. So if you define a supervision scope, everything inside it no matter how complex the async graph is, it'll all cancel/retry appropriately at that scope.
The scope will also guarantee that when we leave the scope, all concurent tasks are done, nothing is left running, either because it's all cancelled or the scope waits for all tasks to complete.
Here's some good articles that explains the motivation for it:
https://elizarov.medium.com/structured-concurrency-722d765aa...
https://github.com/apple/swift-evolution/blob/main/proposals...
For example, Rust's `Future` is structured (although executors may introduce escape hatches), while JavaScript's `Promise` is unstructured.
As far as async runtime fragmentation you can write code that is generic across runtimes, it's just less convenient to use. If your code only works with plain data and futures you're fine on any runtime, it's when you want to spawn new tasks or block on a task that you have to have a runtime dependency. Async vs sync is the classic "what color is your function" problem which green threads kind of solves but not completely so you still have to understand what is happening so you don't stall every task on a native thread with CPU heavy or blocking work.
How does futures.rs work generically with different async runtimes then?
This is true, but I think there are a few notable caveats:
1. Although you can't generically build libraries per runtime, it is possible to right a library supporting an explicit set of runtimes with some boilerplate; the simplest way is to just to abstract all of the async primitives you want into an API to use internally, use feature flags to implement them based on which runtime you want to support, and then only use that API in the rest of your library (which both avoids the need for conditional compilation outside of that one wrapper module for async stuff and reduces the surface of places you'd need to update to add or remove support for a runtime or if you want to update a runtime to a version that isn't backwards compatible.
2. Although there are quite a lot of runtimes that exist in the ecosystem right now (and more could enter the scene in the future), in practice the usage of a lot of these runtimes is quite small. For a few examples, at the time of my looking up, tokio has 66.7 million downloads overall and 10.3 million "recent" ones; async-std has 9 million overall and 1.3 million recent; smol has 1.6 million overall and 158k recent. There's diminishing returns for each new runtime you add support for, so while there's some up front burden in terms of supporting more than one runtime (like I describe above), the burden over time is not going to be super high.
3. Rust has a history of starting out by providing lower-level primitives for things, letting the community iterate over various ideas in the space for a few years, and then eventually settling on a single or small set of pseudo-official crates for them. I think the error handling APIs are the best example of this; Rust 1.0 launched with the standard library Error trait and the `try!` macro, and the community iterated over a bunch of potential solution (error-chain, failure, and probably a bunch of others I don't even remember at the moment), and for a few years it was a bit messy. Meanwhile, the standard library added the `?` operator and added a replacement for one of the `Error` trait methods (deprecating the old ones), and eventually the churn settled down and most people just use `thiserror` and `anyhow` now. There's still some lingering things to standardize like backtraces for errors, but overall the error handling space is way less fragmented now than basically at any point in the past. I'd argue that the async runtime churn is already starting to trend towards equilibrium; if this turns out to be the case, the costs of supporting multiple runtimes will continue to go down, and I'm guessing (hoping?) that within a few years people will have either mostly standardized on a single runtime or we'll have a standard solution for wrapping the small set of runtimes that retain any significant usage in the community.
2. Tokio has the best support right now but Tokio is very difficult to write libraries for and may not be the best designed async library. Monoio looks like a very good design and may be the best option for web servers because you don't have to deal with Send and Sync.
3. This method usually works but for async they should have been more opinionated. Stackfull coroutines line [May](https://github.com/Xudong-Huang/may) would have worked best for 99% of the use cases and maybe there could be a separate no-cost async setup for embedded. But trying to have both under the same async paradigm is causing huge issues.
Which is true around the M:N stuff, but the missing piece is the communication. Go has channels, Erlang uses the actor model, do you know if there are any plans to have something like this in Java?
Frequently I've written applications that use Executors, Reactive Streams, etc. and found that frameworks like that rarely, if ever, provide a good answer for how to tear a processing system down at the end. It's completely practical to use a stream processing framework to process batch jobs, but to get correct answers you have to shut the thing down at the end. I've frequently built teardown systems and it's always left me with the feeling that system programmers should occasionally climb down from their high horse and write an application.
That's one of the things structured concurrency improves on. By defining a scope in a try-with-resources block, you make sure it's teared down at the end of whatever operation you perform.
In the systems I've built there are frequently a few queues and also some resources such as a MapDB or Jena instance, "shutting down" is a matter of flushing out the queues and closing the resources in dependency order.
One of them used the Jena Rules Engine as a control plane, effectively a group of production rules that build all the queues, initialize the processing elements, resources, etc. I got told by the Jena people that this use case was not supported but it worked pretty well.
From a Kotlin co-routine point of view Loom is about the JVM gaining some low level optimization features that will slightly enhance co-routine performance on newer JDKs (not that this was actually much of a problem) but will otherwise not really add anything new in terms of features as co-routines already had pretty much all of the features.
Of course it's nice for Java to gain a few new APIs for this. But it's not like there was a shortage of asynchronous programming APIs. If you intend to use this as a drop in replacement for Threads, read up on blocking vs. non blocking IO or you'll be finding out the hard way why Java uses a lot of real threads when dealing with blocking IO. If on the other hand you were already using non blocking IO, using Threads would not have been a thing you'd be doing. Hint, virtual threads in the same thread will all block if you use blocking IO.
With Kotlin, co-routines, the compiler will issue warnings if you call into blocking Java stuff from a co-routine and nudge you to deal with that by of-loading to a threaded co-routine context.
This is apparently not the case in the new JDK implementation of virtual threads. According to JEP 425 (linked in the OP):
> Application code in the thread-per-request style can run in a virtual thread for the entire duration of a request, but the virtual thread consumes an OS thread only while it performs calculations on the CPU. The result is the same scalability as the asynchronous style, except it is achieved transparently: When code running in a virtual thread calls a blocking I/O operation in the java.* API, the runtime performs a non-blocking OS call and automatically suspends the virtual thread until it can be resumed later. To Java developers, virtual threads are simply threads that are cheap to create and almost infinitely plentiful. Hardware utilization is close to optimal, allowing a high level of concurrency and, as a result, high throughput, while the application remains harmonious with the multithreaded design of the Java Platform and its tooling.
So new Java virtual threads really are meaningfully different from Kotlin coroutines.
What Loom likely does for blocking IO is implement some continuation wrappers around that do the right thing; which is indeed nice.
With Kotlin co-routines, you'd mostly avoid using blocking IO in favor of using non blocking IO. With the exception of legacy code that uses blocking IO which up until now you'd simply park on a threaded co-routine context for that reason. So, yes this is a very nice feature that I wasn't aware of and thanks for pointing that out. It won't massively change my life because there's not a whole lot of blocking IO happening in most things that I use. But it's great for transitioning legacy code to more modern frameworks without a lot of invasive changes.
Could you elaborate on some of those?
- Coroutines have a higher overhead. Though not huge, Loom threads are nearly free.
- Kotlin Coroutines aren't pre-emptible (and this is the big one). Loom's threads are pre-emptible. This property is why they can be more efficient.Also, Kotlin co-routines will obviously use Loom when it is available and offer the same benefits. All that takes is a few tweaks in the co-routines library. Mostly, that should not even be needed because Loom is API compatible with the old threadpool and thread APIs which already have extension functions to turn them into coroutine scopes. So, a lot of stuff should just benefit from Loom just by upgrading the JVM. Which is great of course.
The otherwise problem is that you end up with two worlds, the non-async ad the async ones, and they can't really call each other without a lot of problems.
The new JVM features will allow you to write async code while still being able to call any functions, that'll make it actually useful.
Kotlin's co-routines are simply syntactic sugar for simple callbacks but without the boiler plate. If it has some kind of success / fail callback mechanism, you can create a co-routine from it. Futures, Promises, a Flux, whatever. That's all that happens and the co-routine library comes with extension functions for all these.
I'm not sure what you measured relative to what. But I bet it is some blocking/expensive code actually taking a while before it suspends or something similarly sub-optimal. Of course the key bottleneck with virtual threads would be the scheduler loop that runs all these co-routines. The main contribution of Loom is moving that stuff into the JDK and possibly optimizing the hell out of that. But other than that it does something very similar to what other green/light weight threads have to do necessarily.
Basically any time any virtual thread does something expensive/blocking, all the other virtual threads suffer. If you see a browser app freeze for a bit, that's usually what happened. Loom adds a thing called continuations which literally have a suspend method that tell Loom that it is now safe to switch to another Fiber. Failure to use suspend correctly, will result in the same stuttering behavior on Loom: things will freeze or block until whatever is running calls suspend: only 1 virtual thread executes at the same time. That's the whole point. For non blocking IO this is fine. If you are doing computationally intensive stuff, you either need to suspend frequently or use some thread pool. Otherwise the virtual thread will hog the CPU whenever it runs.
As for the two worlds, I can call any function from a suspend function. Just not the other way around without creating a co-routine context. What Loom adds is basically a better light weight thread scheduler. Under the hood it likely does many of the same things that Kotlin already did.
Most Java APIs like socket and file have been re-written to do this for you under the hood.
You're not expected to manually call suspend.
> Just not the other way around without creating a co-routine context.
This goes beyond function calling though. Same restrictions apply for implementing interfaces or passing functions as arguments, right?
With loom, there is just one world. A function is a function is a function. It's good that both Java and Kotlin can take advantage of this improved model.
> Under the hood it likely does many of the same things that Kotlin already did.
Maybe, but this also extends to debugging, heap dumps and stack traces. Having this builtin has a lot of benefits.
The problem with the Kotlin implementation is that if you ever want to be able to call a single async function, every call higher in the stack must also be async (unless you have a separate runner, but that's not always possible, and even if it is you can have surprising results)
That's only partly true. And yeah they're callbacks but not like e.g. we know from JS.
As far as I know CPS basically means that the Kotlin compiler turns a function like that:
suspend fun myFunction(param: String) {
val asyncVal = someSuspendingFunction()
val asyncVal2 = anotherSuspendingFunctio(asyncVal)
return someCalculation(asyncVal2)
}
Into a modified method and *class* like that: // The different parts of the function get put into different ifs
// The state that is normally in the JVM stack frame is holded in a custom class which is a subclass of https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-continuation/
// Some integer tracks where in the function we are
fun myFunction(param: String, continuation: Something<MyFunctionClass>?) {
continuation = continuation ?: MyFunctionContinuation(param)
if (continuation.state == 0) {
contination.state = 1
someSuspendingFunction(continuation)
return SUSPEND
}
if (continuation.state == 1) {
// The result mechanism is in the Contiuation interface/abstract class. It can be used to later receive the async result
// of callig annother suspending function
continuation.asyncVal = contiuation.getResult()
anotherSuspendingFunction(continuation.asyncVal, continuation)
return SUSPEND
}
if (continuation.state == 2) {
continuation.asyncVal2 = contiuation.getResult()
return FINISHED(someCalculation(continuation.asyncVal2))
}
}
class MyFunctionContinuation: Continuation {
// Function arguments
val param: String
// Every suspending function needs this. It tracks where in the function we currently are
val state: Int = 0
// Local variables inside the function. Those would normally live in the JVM function stack
// But the Coroutine runtime needs to be able to "rehydrate" the JVM function stack once a non-blocking call finishes
var asyncVal: <Don't remember>
var asyncVal2: <Don't remember>
}
I hope the basic idea comes across.I read this sometime in some book about coroutines. But you can see that there is some overhead. The basic idea is to replicate the function call stack in a custom datastructure. This way we can be deep inside a normal JVM callstack and also have a separate Kotlin Coroutine callstack.
Once the coroutine wants to suspend it can simply return from the JVM callstack but the Kotlin callstack is preserved. Later when we want to continue we can call the same function again, with the Kotlin callstack now containing the result of the async operation.
If the JVM can do this natively we wouldn't need this second Kotlin coroutine callstack because the native JVM callstack would support this.
https://paulhoule.github.io/pidove/apidocs/com/ontology2/pid...
that involve a slightly awkward state machine (and some complexity around initialization and teardown) that can be avoided with coroutines such as Python's generators.
* clean call stacks - stack traces returned by current async libraries are very difficult to understand
* no need to colour functions, no need to worry if your function is suspendable or not and in which context you are using it
Java record pattern matching in JDK 19 - https://news.ycombinator.com/item?id=31378896 - May 2022 (155 comments)
Using Java's Project Loom to build more reliable distributed systems - https://news.ycombinator.com/item?id=31314006 - May 2022 (91 comments)
JEP proposed to target JDK 19: 425: Virtual Threads (Preview) - https://news.ycombinator.com/item?id=31236855 - May 2022 (212 comments)
Achieving 5M persistent connections with Project Loom virtual threads - https://news.ycombinator.com/item?id=31214253 - April 2022 (145 comments)
Loom: Project Loom Early-Access Builds - https://news.ycombinator.com/item?id=28191308 - Aug 2021 (76 comments)
Project Loom and Structured Concurrency - https://news.ycombinator.com/item?id=25300233 - Dec 2020 (108 comments)
Project Loom: Fibers and Continuations for the Java Virtual Machine - https://news.ycombinator.com/item?id=15599854 - Nov 2017 (41 comments)
However, given the number of excellent JVM languages, especially Kotlin, I'm curious what the general consensus is for starting new projects. That is, if I were starting a greenfield project, I think I would definitely use Kotlin over Java - Kotlin doesn't have to deal with the backwards compatibility concerns of Java, and thus does away with some of the problematic annoying complexity of Java. E.g. first class language support for nullable/optional types I think is critical these days - Java has bolted on an Optional wrapper, but it's cumbersome and doesn't provide the kind of strong guarantees that a language-implemented version does.
So what does the HN community think? Would you use another JVM language over Java if you were starting a new project?
Java is coming along quite nicely. Its pattern matching looks to be better implemented than Kotlin's (e.g. destructuring implementation). Same with string interpolation (https://openjdk.org/jeps/8273943). And now with Loom, no need to maintain two separate API surfaces for sync vs async. It has the last mover advantage.
Some years ago, my division at <large company> decreed Scala was the way of the future. All new development was to be done in Scala. We were offered training classes. We formed book clubs. We paired, shared, and opined as we tried really hard to do functional programming correctly. We genuinely tried, and then we gave up. The specific reasons would better suit another post, but it was a grassroots developer-led effort that led us to abandoning Scala.
At this point, we have backend code in NodeJS, Scala, GoLang (this is currently a performance-sensitive one-off), and Java. That's a problem for code reuse, tooling reuse, and general maintenance. If we were to write new components in Kotlin, we would be making the problem worse. At this point in time Java is our safe fallback.
In the future we may try a different language again, but for now there needs to be a really good reason to add to our tech stack. I suspect most new projects will continue to be Java for quite some time, because we're not going to rewrite all the Scala and NodeJS code "just because". It will slowly get replaced as it stops working, or as we find reasons to replace it other than "it's in a language we don't like". Until then, our polyglot set of codebases is too intimidating and devs are going to resist adding another language to it.
I don't agree with this. Kotlin is essentially a drop-in replacement for Java, so much so that you can replace your Java classes _one by one_ with Kotlin classes. The build tools, frameworks etc are mostly the same, so no real changes required in the environment. (All this is not true for Scala, for example.)
From one perspective, one might argue that Kotlin didn't do enough (eg. it has ADTs but no pattern matching), on the other hand, this is exactly why it is so easy to step up. You can also very quickly get productive in it coming from Java, there is no need to use advanced stuff like async/await, and there are very few caveats you need to worry about.
Just my 2c, based on experience.
https://typelevel.org/cats-effect/
The language, the community and customs are great. You don't have to worry about nulls, things are immutable by default, domain modelling with ADTs and patter matching is pure joy.
The tooling available is from good to great and Scala is big enough that there are good libraries for typical if not vast majority of stuff and Java libs as a reliable fallback.
It's not some panacea though, and at the end of the day Java is the system language of the JVM. New VM features are not built to cater to the guest languages. In the case of virtual threads we have a directly competing "async" solution to Kotlin coroutines. So you may see further ecosystem split between libraries catering to Android and those catering to backend work.
no. I'm never a fan of anything like typescript or Kotlin where it's transpiled (yes i know Kotlin is compiled down to JVM byte code) end up dealing with more headaches and troubleshooting in the end.
First, yes, I agree that setting it up initially and understanding all the various tsconfig options can be a pain. But it's also largely a one-time cost, and honestly over the past couple years I really didn't have any "weird troubleshooting" problems once I got it working. Also, these days more is being done to make TypeScript easier to run out of the box, e.g. Deno.
But the benefits of using TypeScript are vast, even from an individual programmer perspective, nevermind the huge benefits of using it to allow for refactoring and better comprehension in a larger team. The autocomplete benefits alone that I get with it in VSCode are worth it.
[1] https://openjdk.org/jeps/405
See how sucessfull Kotlin/JS and Kotlin/Native are on the wild.
The same libraries that can be used from Kotlin?
The toolchain in both cases is basically IntelliJ (rebranded as Adroid Studio)
At that point one could also say "why Anrdoid?".
Now if you plan to dynamically link to Java libraries that require JVM opcodes without counterparts on DEX, SIMD, Panama, Java Standard Library post Java 11, then thought luck.
See it as an opportunity to learn how to use the NDK (for SIMD), or write your own Kotlin versions.
Kotlin was designed to replace Java 1.6 and that's about all it's good for.
Not sure what K was designed for. But we use it to fix J's billion dollar "implicit nulls" mistake, and terse syntax (making it useful for templating: Kotlinx.html), and pass-as-value method references. We love it's hang towards immutability.
K is not Haskell, but it sure feels a lot safer that J (where a lot of strings were used to refer to methods, and strings+Groovy was used to do templates).
Bottom line: I'm too old --and life's too short-- for NPEs.
I moved from Java to Node (then Typescript) about 8 years ago. Moving to a language that doesn't allow implicit nulls makes a world of difference, importantly, at "programmer" time, not just build time.
That is, when programming in VSCode, I immediately get feedback if I attempt to use a nullable variable in an unsafe manner. That has huge productivity benefits compared to if I only got that feedback after my build ran.
Similarly, simple things like safe optional chaining (i.e. foo?.bar?.baz) makes code so much faster and easier to write, and AFAIK this hasn't been added to Java yet (correct me if I'm wrong).
We went from having RAD IDE's that produced native UIs to everything has to be done in electron because nobody is innovating in the Desktop GUI space.
>And all UI development was happening with web tech, where somehow the divergence didn't matter.
If you're 95-99% of time using non-native web technologies, why would you care about that much about rest of it being native?
Very much nicer this way, and although a few businesses are still stuck on 6 or 8 most places seem to be aiming to stay current with Long Term Support (LTS) releases (of which 17 is the current LTS and 21 will be the next one).
I am hoping that many of the things in the pipe right now are finalized for 21.
Some of the innovations were value types extensions, AOT compilation or JIT caches, and until OpenJ9 came to be, that usually was only available to commercial clients.
I mean, I'd like to see better typing because the current generics are too basic, and you always end with casts and `isAssignableFrom`. Is there anyone working on types for Java, like in TypeScript and Rust?
Another huge issue for me is null-type safety. The latter is basically the only reason we use Kotlin right now. We would be happy to migrate to the latest Java if it fixes the nulls. I'm wondering what prevents adding a simple syntax sugar like optional !-suffix for non-nullable definitions. Like `void fooBar(String! path)`, which should prevent at the compiler level from passing a null argument. Could that work?