News for hackers. Stuff that matters.
Re:Java 21 Released by lambda_garden on Tuesday September 19, 2023 @10:48AM (#37570723)
I have a genuine question about Loom / virtual threads / etc.

In languages with async-await syntax, I like that it is explicit what is sync and what is async. I also like how the "bind" points within an async expression make it clear where the syncronization happens. It seems to me that Java 21 makes all of this implicit, which is a step backward in this respect.

Similar to how types enforce valid usage (a foundational idea in Java), I feel that an Async<T> should not be implicitly a T.

Are people concerned about this at all?

Have the Java designers addressed this?

Re:Java 21 Released by jayd16 on Tuesday September 19, 2023 @11:35AM (#37571446)
It's a different priority, not a down grade. Most people in Loom discussions really don't understand why cooperative async is set up the way it does and why explicit yielding is important if you need to manage a single UI or GPU bound thread. Most of the replies you get are just thinking about raw bandwidth of nameless threads.

But the structured concurrency stuff they added is sorta kinda familiar to explicit yields (as long as you set up everything properly). IMO its uglier than async/await but I also deal with main threads a lot.

I'm excited to see if this breeds any interesting UI frameworks or if its mostly just about server bandwidth.

Re:Java 21 Released by lbalazscs on Tuesday September 19, 2023 @11:49AM (#37571712)
The question of "single UI or GPU bound thread" could be solved in a future release with custom virtual thread schedulers. Java 21 concentrates on solving the problem of server-side applications.
Re:Java 21 Released by jayd16 on Tuesday September 19, 2023 @07:46PM (#37578660)
I think it would require new paradigms, though. The existing popular patterns assume cooperative concurrency. We've had preemptive threading for a long time and UI programming has not made inroads with it. I don't see how using virtual threads makes this an easier lift.

My fear is that we'll end up living with a bolted on async/await style for this stuff but with worse syntax. Still, I'm excited to see how it shakes out.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @11:06AM (#37570971)
I agree with you—I prefer explicit synchronization. But this isn't an accidental flaw in Java's design, it's the entire purpose of their chosen model. A lot of people dislike the fact that async/await causes function coloring [0] and as a result may lead to duplication of sync and async version of functions. These people see Java as dodging a bullet and learning from past mistakes in other languages, so it's unlikely that Java will have anything comparable to async/await.

[0] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

Re:Java 21 Released by lambda_garden on Tuesday September 19, 2023 @11:28AM (#37571334)
Java 21 is colorblind - but the colors still exist
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @11:30AM (#37571370)
Yep, I agree. I tend to be a type system maximalist: I'd rather have everything that is possible to encode statically be encoded statically, even if it leads to less flexibility later.

If you're starting a new Java codebase from scratch, you can create the structure where the language doesn't using classes, but it'd be nice to have syntactic support for it.

Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @11:37AM (#37571469)
This property is not really part of the type system though.

Also, as mentioned by another commenter below - is a file exists operation async? Sync? Why, why not?

Re:Java 21 Released by wiseowise on Tuesday September 19, 2023 @11:51AM (#37571747)
What if file exists operation MUST be sync?
Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @11:57AM (#37571855)
Then you don’t start it in a virtual thread. Your calls automatically become blocking or not blocking depending on your callsites, which is the proper place to decide that.
Re:Java 21 Released by wiseowise on Tuesday September 19, 2023 @11:59AM (#37571880)
So it still has colors?
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @11:56AM (#37571828)
In TypeScript at least, async-ness is encoded in the type system, because async/await is just syntactic sugar for Promise/then/catch.
Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @12:42PM (#37572565)
Well, JS is paired with a single-threaded execution model, so you only have concurrency to worry about. C#, Java has proper multithreading as well, that gives another dimension to the problem.
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @01:27PM (#37573231)
Kotlin has multithreading, and it also puts `suspend` in the type system.
Re:Java 21 Released by n2d4 on Tuesday September 19, 2023 @02:56PM (#37574590)
Some more examples: Rust, Python (with most/all? type checkers), C# all bake this into their type systems.
Re:Java 21 Released by vips7L on Tuesday September 19, 2023 @05:02PM (#37576589)
This is more due to the fact that Kotlin doesn’t control its backend and is designed to work with a language that already includes this model, js.
Re:Java 21 Released by samus on Tuesday September 19, 2023 @08:26PM (#37578946)
Very simple - in Java, almost any IO operation in blocking. Exceptions are the stuff in the NIO package, thread interruption and signalling, and maybe process termination.
Re:Java 21 Released by jayd16 on Tuesday September 19, 2023 @11:37AM (#37571475)
I wonder if we'll see Lombok markup for @OnVirtualThread or whatever the pattern shakes out to be.
Re:Java 21 Released by andrewaylett on Tuesday September 19, 2023 @11:57AM (#37571842)
Lombok is unfortunately a separate programming language, albeit insufficiently distinct from Java.

(Why? Because it uses internal compiler APIs to change the behaviour of code you write in ways that are not valid: annotation processors are supposed to generate new classes, not mutate existing classes.)

Re:Java 21 Released by samus on Tuesday September 19, 2023 @08:30PM (#37578981)
I went looking on their because for some reason I thought that an `@Async` annotation would already exist. But no such luck.

If you want to use something like this today, then you could take a look at Spring 's Async support. It also lets you supply the Executor.

Re:Java 21 Released by mike_hearn on Tuesday September 19, 2023 @11:18AM (#37571153)
I'm not personally concerned, no. You need to think about threads to efficiently exploit multi-core hardware anyway. Otherwise the best you can do is hope you're implementing isolated requests with multi-processing, and assume that you'll never benefit to exploit intra-operation parallelism.

One problem with async/await is that the term "blocking" is under-specified. If you try and nail down what is and isn't blocking, like with a formal way to classify operations as such, you rapidly end up with a lot of contradictions and gray areas. It's a distinction that doesn't really make sense except as a hack to work around expensive threads/callstacks, which is what Loom solves.

For example, is reading a file blocking? On modern computers this could do anything spanning from being purely a set of branches and pointer dereferences as a kernel block cache hit, thus being little different to any other code, or it could be an NVMe flash request (extremely fast, probably not worth switching the thread), or it could be an HDD seek (fairly slow, worth freeing up the thread to do something else), or it could be a network RPC (extremeley slow, definitely worth freeing up the thread). You may not even know ahead of time what it is. So, should the API be blocking? Or marked async?

The right place to figure this out isn't in hand-written code. That'd be a performance hack similar to the old C register keyword. The runtime has much more information on which to make the right call, but for that, the code has to be written in a uniform style.

More generally, type systems ideally aren't encoding performance-specific stuff into them. Type systems help with program organization and correctness, and as a happy side effect can also guide compilers to make better code, but the performance of an operation shouldn't itself be exposed in its prototype. Otherwise optimizations can break code, which wouldn't make sense, being able to optimize modules independently is a big part of why you want dynamic linking.

Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @11:08AM (#37570997)
Structured concurrency does give you a “structure” on possible fork and join points, for a more high-level management.

Within virt threads, does it really matter? Every function takes as much time as it takes - the reason you use virtual threads is to make that efficient, the semantics is clear - your code is blocking, and waits for every previous statement.

Also, it is not quite valid to model it as types - these are more like effects.

Re:Java 21 Released by djur on Tuesday September 19, 2023 @11:04AM (#37570940)
Why do you like that? What problems has it helped you solve or avoid?
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @11:13AM (#37571076)
Not OP, but I agree with them. Here's my take:

If you design your software right, you can use async/await similarly to IO in Haskell—functions that are not async are fast and unlikely to fail, while functions that are async are potentially slow and likely to throw exceptions. This partition can help you reason about and structure your code as you're writing it for solid error handling and minimal duplicate I/O.

Re:Java 21 Released by hocuspocus on Tuesday September 19, 2023 @11:32AM (#37571401)
Yes but Java is not Haskell.

If you want a monadic IO on the JVM, Scala offers two ecosystems with extremely capable effect systems, providing highly performant runtimes and good (but not perfect) ergonomics.

I've always found async/await bolted on older, mostly imperative OO languages with exceptions to be pretty disappointing. Even modern designs on languages with proper error handling like Rust get their fair share of criticism.

I get that virtual threads will probably introduce surprising bugs and performance issues in some corner cases. But it also removes a whole lot of craziness in the Java ecosystem, namely reactive programming.

Re:Java 21 Released by pgwhalen on Tuesday September 19, 2023 @11:33AM (#37571409)
It feels like that's coupling some concerns together. Functions that do IO are more likely to fail, yes, but that doesn't have to do with their async-ness or their slowness.
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @11:40AM (#37571526)
It's not that async-ness causes things to fail more easily, it's that if you're going to make a function async it's because it interacts with the outside world in some fashion, and interaction with the outside world is slow and likely to fail.

We could use a different name for that concept to make it more clear why these ideas are related, like `IO`, but in practice async=IO in most code.

Re:Java 21 Released by pgwhalen on Tuesday September 19, 2023 @12:27PM (#37572323)
It seems like we're on the same page. My point is that your last comment...

> in practice async=IO in most code

does not need to be true. We will never avoid the complexity of IO, but we can avoid the complexity of async, and virtual threads provide us that opportunity.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @12:44PM (#37572593)
I didn't mean that = to be transitive. All async is IO, but obviously some IO is not async.

> We will never avoid the complexity of IO, but we can avoid the complexity of async

Because we can never avoid the complexity of IO, having a language construct that makes it obvious when you're using IO is valuable. IO code is different in kind than non-IO code, and having it explicitly marked helps keep the mess that is IO from propagating its problems throughout the rest of your code.

Re:Java 21 Released by pgwhalen on Tuesday September 19, 2023 @01:09PM (#37572999)
I don't entirely disagree that treating IO as something special at the linguistic level is a good idea, but I do think that async is a wildly indirect and confusing way to achieve that.

Language designers don't add async to make IO look special, they add it as a concurrent programming model.

Re:Java 21 Released by samus on Tuesday September 19, 2023 @09:11PM (#37579251)
When IO fails, it doesn't matter a single bit whether the call occured on a virtual or on a platform thread. The likelyhood of failure deserves coloring, but Java already offers a mechanism for that.
Re:Java 21 Released by bcrosby95 on Tuesday September 19, 2023 @11:51AM (#37571737)
Java has checked exceptions, so you don't need anything to signal that it is "likely", you can literally declare it in your code and have the compiler tell you if you're screwing up.
Re:Java 21 Released by za3faran on Tuesday September 19, 2023 @12:55PM (#37572797)
So this might actually make checked exceptions popular again eh? ;)
Re:Java 21 Released by bcrosby95 on Tuesday September 19, 2023 @02:04PM (#37573770)
I know they cause their own problems, but I generally prefer checked exceptions over "read the non-existent documentation" exceptions.
Re:Java 21 Released by flanked-evergl on Tuesday September 19, 2023 @02:06PM (#37573805)
> functions that are not async are fast and unlikely to fail, while functions that are async are potentially slow and likely to throw exceptions.

That would seem like using the wrong tool for the job. If you want to colour functions according to reliability and speed you should use a different mechanism to colour them than a mechanism designed to colour functions based on how they behave if IO blocks.

Re:Java 21 Released by jayd16 on Tuesday September 19, 2023 @11:59AM (#37571878)
A common pattern is to use a single thread executor as one context. Any task scheduled to that context will not run concurrently. When you do it this way, every synchronous chunk (between yields/awaits) is a critical section. So you get critical sections easily, cheaply and with a pretty good syntax.

This pattern is used a lot when you have things like UI which use a single threaded design, or if you need to bind to do some IPC in a single native thread. It's actually very common.

Re:Java 21 Released by pjmlp on Tuesday September 19, 2023 @11:18AM (#37571161)
They also have enough gotchas that one of the main ASP.NET architects had to write a best practices document to address all common misuses.
Re:Java 21 Released by neonsunset on Tuesday September 19, 2023 @06:40PM (#37578022)
Some of the recommendations in the list date back to .NET Framework.

Most of the time, it is not something one needs to worry about because actually problematic cases are highlighted by default Roslyn analyzers and will give you a warning on build as well.

Async/await is a superior model that is more flexible and solves more tasks (heh) at hand than green threads which have more limited scope of application, higher base overhead and cannot reach parity at efficiency due to stack state having to be always preserved.

Re:Java 21 Released by kaba0 on Wednesday September 20, 2023 @03:08AM (#37581267)
Stating something doesn’t make it so.
Re:Java 21 Released by lern_too_spel on Tuesday September 19, 2023 @11:09AM (#37571009)
Why should that matter? If you want to run two tasks concurrently, you run them concurrently the same way, whether they are blocking or not.
Re:Java 21 Released by lbalazscs on Tuesday September 19, 2023 @11:46AM (#37571646)
Ideally knowing "what is synchronous and what is asynchronous" shouldn't be your concern, just like freeing memory isn't your concern in a language with garbage collection. Similarly, you don't have to know the "real" memory addresses when using virtual memory.

Ask yourself why do you want to know what is asynchronous. In a typical server-side app synchronous, blocking, thread-per-task code is easy to write and read. There is only one drawback: you can run out of kernel threads. Asynchronous code solves this problem, but virtual threads solve it in a better way, because you can have three orders of magnitude more virtual threads compared to kernel threads. Ultimately, it's all about the number of threads you are allowed to use.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @12:47PM (#37572640)
Async vs green threads doesn't alter the number of running threads available to you, it's simply a difference in how the runtime schedules your virtual threads on the limited number of real threads. Async/await is cooperatively scheduled, Java's virtual threads are preemptively scheduled.

Both can have similar performance characteristics, the difference is primarily one of ergonomics, and since that's subjective there are people who will argue fiercely for either side.

Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @01:23PM (#37573178)
I’m not really sure that it is all that subjective — plain old serial, blocking code is objectively easier to reason about than “jumping around” code.

It’s a different question that async-await may have a few very specialist use-cases, that is not readily available in the preemptive model.

Re:Java 21 Released by vips7L on Tuesday September 19, 2023 @05:06PM (#37576644)
> blocking code is objectively easier to reason about than “jumping around” code.

This is precisely why await was introduced in the first place. Promise and callback code was impossible to reason about.

Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @05:08PM (#37576696)
A Promise is not ordinary blocking code.

String res = someRestCall();

is.

Re:Java 21 Released by vips7L on Wednesday September 20, 2023 @05:11PM (#37590244)
I was agreeing with you. They introduced await because they know that

    var s = await someCall();
is way easier to reason about than:

    someCall().then(…)
Re:Java 21 Released by kaba0 on Thursday September 21, 2023 @02:01AM (#37593758)
Oh, I see! Sorry for the misunderstanding!
Re:Java 21 Released by samus on Tuesday September 19, 2023 @08:49PM (#37579108)
Major nitpick: also virtual threads are cooperatively scheduled on carrier threads (the underlying platform threads). You "cooperate" implicitly by calling APIs that block the thread. There is no preemption if you hog the carrier thread with, say, number crunching or array sorting.

Straight from JEP 444:

> The scheduler does not currently implement time sharing for virtual threads. Time sharing is the forceful preemption of a thread that has consumed an allotted quantity of CPU time. While time sharing can be effective at reducing the latency of some tasks when there are a relatively small number of platform threads and CPU utilization is at 100%, it is not clear that time sharing would be as effective with a million virtual threads.

Re:Java 21 Released by wiseowise on Tuesday September 19, 2023 @11:49AM (#37571711)
That’s the idea behind suspend keyword in Kotlin:

https://elizarov.medium.com/how-do-you-color-your-functions-...

Re:Java 21 Released by aardvark179 on Tuesday September 19, 2023 @11:52AM (#37571779)
In many languages the distinction is there because you have two versions of the IO routines, synchronous and asynchronous, and calling a synchronous one will block you asynchronous task. With virtual threads we took a different approach: if you make an IO call on a virtual thread it will do the IO asynchronously if at all possible and yield execution. This means you can take existing code and run it on virtual threads without modification and it will pretty much just work.

Not all IO operations can be done asynchronously on all platforms, and some new interfaces had to be introduced to allow for asynchronous name lookup, and some code needs refactoring because it had implicit assumptions that it would be used by tasks in a small thread pool and so used thread local values as a cache for expensive objects, but in general I think the design has achieved the goal of allowing existing code to be moved to virtual threads without the requirement to rewrite everything or scatter await / async all over the place.

Re:Java 21 Released by svieira on Tuesday September 19, 2023 @12:01PM (#37571916)
This is an old discussion, Unyielding (https://glyph.twistedmatrix.com/2014/02/unyielding.html) discusses the trade-offs between function coloring and explicit transaction control from the point of view of "I need explicit transaction control" and of course What Color Is Your Function (https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...) discusses it from the point of view of function coloring. Which one you want depends on your application, unfortunately.
Re:Java 21 Released by pron on Tuesday September 19, 2023 @12:32PM (#37572400)
Yes. The preemptive style (where scheduling points are determined dynamically) is more composable than the cooperative style (where scheduling points are determined statically). In the cooperative style, the correctness of any subroutine is implicitly dependent on there not being scheduling points where they're not explicit; adding a scheduling point to any subroutine, therefore, requires reconsidering the assumptions in all of its transitive callers. In the preemptive style, on the other hand, any subroutine can independently express its mutual-exclusion assumptions using various synchronisation constructs, such as locks (those independent decisions could result in deadlocks, but the cooperative style is also subject to deadlocks).

So if you start from scratch, the preemptive style is preferable, but for us the decision was even easier, because we weren't starting from scratch. Java already had threads, and so mutual exclusion was already explicit rather than implicit.

When is cooperative scheduling more appropriate? In a language like JavaScript that already had a lot of existing code with implicit mutual exclusion assumptions (because JS didn't have threads). To add concurrency to such a language with a large existing ecosystem, cooperative scheduling is a better choice.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @01:03PM (#37572920)
> So if you start from scratch, the preemptive style is preferable

I don't believe that you proved that in the preceding paragraph. All you said is that cooperative scheduling requires making the scheduling points explicit while preemptive doesn't, but "explicit is better than implicit" is a principle that a substantial portion of the programming community believes, and OP specifically expressed a preference for explicit scheduling points.

For myself, I also want to see scheduling points propagate up the call tree the same way that I want checked exceptions. The lack of composability is a feature, not a bug. I prefer to know which parts of my code are most likely to be problematic and then keep those parts isolated from the rest.

I recognize that this is a subjective opinion and not everyone believes what I do, and for some use cases preemptive is better, but I don't see why you need to pretend that you made the most optimal decision instead of just acknowledging that it's complicated and you made the one that you feel is best for Java.

Re:Java 21 Released by pron on Tuesday September 19, 2023 @01:12PM (#37573050)
> All you said is that cooperative scheduling requires making the scheduling points explicit while preemptive doesn't, but "explicit is better than implicit" is a principle that a substantial portion of the programming community believes, and OP specifically expressed a preference for explicit scheduling points.

The problem is that the cooperative style makes mutual exclusion implicit and that's the important correctness assumption that is explicit in the preemptive style. The possible presence of scheduling point is the lack of an assumption, and therefore it shouldn't be explicit.

You're right that the issue is subjective, but it's subjectively determined in the overall design of the language. In Haskell, for example, there is an ever-present default assumption of lack of side effects and that assumption is at the core of the language, but if your language doesn't do that -- and Java doesn't and neither do C# or Python -- then the choice of cooperative scheduling isn't a good fit for the rest of the language. So perhaps I should have said that unless you design your language like Haskell (and unless you don't have the constraints the JS does), preemptive is the better fit for the rest of the language.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @01:23PM (#37573193)
There's nothing to stop you from setting up synchronization constructs like mutexes in a cooperatively-scheduled application. Kotlin bundles them with kotlinx.coroutines [0]. If anything, having possibly-concurrent methods explicitly marked makes mutual exclusion easier to reason about, because you know exactly which functions may have concurrency problems.

Are you saying that there's a mechanism in Loom that somehow forces you to mark mutual exclusion which is absent from cooperatively-scheduled languages?

[0] https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-corout...

Re:Java 21 Released by pron on Tuesday September 19, 2023 @01:33PM (#37573319)
> There's nothing to stop you from setting up synchronization constructs like mutexes in a cooperatively-scheduled application.

Right, but assumptions of mutual exclusion are still implicit, and so you must carefully analyse them any time you add a scheduling point. I.e. code may implicitly rely on mutual exclusion in cooperative scheduling but not in preemptive scheduling.

> Are you saying that there's a mechanism in Loom that somehow forces you to mark mutual exclusion which is absent from cooperatively-scheduled languages?

It's not a mechanism in virtual threads but in Java threads in general. Yes, to mark mutual exclusion you must employ some synchronisation construct explicitly, and that is not the case for cooperative scheduling where mutual exclusion is implicit, and that's what makes cooperative scheduling less composable.

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @01:39PM (#37573399)
I think we're talking past each other.

Can we make this concrete? In what way is mutual exclusion implicit in Kotlin coroutines that is not also true in Java Loom?

Here's Kotlin's explanation of how to do mutual exclusion [0], and here's a blog post explaining how mutual exclusion works in Java [1]. The main difference I see is that Java's `synchronized` is a full syntax construct while Kotlin uses its trailing-lambda syntax, but that's not semantically significant.

What do you see as more explicit in the Java version?

[0] https://kotlinlang.org/docs/shared-mutable-state-and-concurr...

[1] https://www.baeldung.com/java-mutex

Re:Java 21 Released by pron on Tuesday September 19, 2023 @02:07PM (#37573814)
Kotlin is not really a good example because it offers the worst of both worlds in this area: You must account for both preemptive and cooperative scheduling because both occur at the same time (they had no choice because they don't control the platform, only the frontend compiler). But let's look at a language like JS, that offers "clean" cooperative scheduling, and compare that to Java.

An async JS function can contain code like this:

    x = 3;
    foo();
    print(x);
Now suppose that you want to perform some I/O operation in foo, which would require changing the function to:

    x = 3;
    await foo();
    print(x);
But is that allowed or does our function depend on x (which is shared among tasks in some way) not being changed between the first and third lines? Does the original function rely on the implicit mutual exclusion for its correct operation or not?

In Java, on the other hand, if x is shared among some tasks then, regardless of what foo does, if the method depends on x not changing then the code must be written as:

    try (xGuard.lock()) {
        x = 3;
        foo();
        print(x);
    } finally {
        xGuard.unlock();
    }
So adding a scheduling point to foo doesn't matter because if our method has any mutual exclusion assumptions it must express them explicitly (in practice, if x is guarded, then the guarding will often be encapsulated somehow, but the point remains). In JS that's not the case, so the addition of a scheduling point requires a careful analysis of all callers.
Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @02:54PM (#37574555)
Does Java actually prevent this code from compiling if `x` is shared?

    x = 3;
    foo();
    print(x);
If not, then I fail to see what the difference is with your JS example. It's still on the developer to keep track of whether x may change (and therefore whether a mutex is required), with the added disadvantage that there's no `async` keyword to clue you in that there might be a problem.

You seem to be describing a style guide rule that Java projects should adopt, not a language feature, and there's no particular reason the same style guide couldn't be adopted another language if required.

Re:Java 21 Released by pron on Tuesday September 19, 2023 @03:07PM (#37574789)
> Does Java actually prevent this code from compiling if `x` is shared?

Why would it prevent it from compiling? Whether it's right or wrong depends on what the programmer has in mind. If they rely on a mutual exclusion constraint, they must specify it.

> It's still on the developer to keep track of whether x may change (and therefore whether a mutex is required), with the added disadvantage that there's no `async` keyword to clue you in that there might be a problem.

No, because in Java, the author of this method has to decide what they want. In JS, when the author of foo decides to change it they need to analyse what the authors of the callers had in mind.

The point is that the presence of a scheduling point or not is not just informative but has an impact on the correctness of the code.

> You seem to be describing a style guide rule that Java projects should adopt, not a language feature

No, it's a language feature. In Java the default is "scheduling point allowed." If you want to exclude it, you have to be explicit. This approach makes composing code easier than the opposite default unless your entire language is built around an even stronger constraint (like Haskell).

Re:Java 21 Released by lolinder on Tuesday September 19, 2023 @03:23PM (#37575036)
> In JS, when the author of foo decides to change it they need to analyse what the authors of the callers had in mind.

No, because they already committed to not having any concurrent code in that method as part of the function contract. Making a function async is a breaking change (the return type changes to Promise<T>). If the author can't avoid the breaking change, then it would indeed force the caller to update their call site, cluing them in that the code may now run in parallel and mutual exclusion should be considered. There's no guesswork at what the callers are doing because you had an explicit contract with them that there would be no concurrency.

If a Java method is passed an object by reference and subsequently decides to modify it in a thread instead of inline, that information is not propagated up to the callers, so they would have either had to guess that the library might change to a concurrent model and make the object thread-safe ahead of time, or they would have to read the release notes and then go find the call sites that need fixing.

Re:Java 21 Released by pron on Tuesday September 19, 2023 @04:40PM (#37576206)
> Making a function async is a breaking change (the return type changes to Promise<T>).

Sure, and that is the essence of non-composability. It's what you want in Haskell, but very much not what you want in an imperative language. And the reason it's easy to see it's not what you want is that those languages, even JS, don't generally colour blocking functions. In imperative languages, blocking is an implementation detail. Deviating from that principle goes against the grain of those languages (though not of Haskell's).

> If a Java method is passed an object by reference and subsequently decides to modify it in a thread instead of inline, that information is not propagated up to the callers, so they would have either had to guess that the library might change to a concurrent model and make the object thread-safe ahead of time, or they would have to read the release notes and then go find the call sites that need fixing.

In Java (and in any imperative language that offers threading, including Rust and Kotlin and C#) any interaction with data that may be shared across threads absolutely requires explicit attention to memory visibility. In particular, in your example you don't need to communicate anything to the caller, just ensure that the method that passes the object to be processed by a thread ensures visibility. This is what the Java Memory Model is about, and thread pools, futures etc. do it automatically.

It's true that JS doesn't require attention to memory visibility, but that has absolutely nothing with cooperative or preemptive scheduling. Rather it has to do with multiprocessing. A language that offers preemptive threads over a single processor will not need any more attention to memory visibility than JS. Conversely, cooperative scheduling in Kotlin, whose scheduler supports multi-processing also requires the same kind of attention.

> I'm just arguing that preemptive is not objectively better, it depends on what existing code is already doing and on what you're trying to accomplish with the feature.

Well, I'm arguing that preemptive is objectively better in imperative languages unless there are external concerns, and that's why Erlang and Go have gone down that route. I know of one exception to that, although the paradigm isn't quite classical imperative, and that is synchronous languages like Esterel [1], but note that the model there isn't quite like async/await and isn't quite cooperative, either. I am not aware of (ordinary) imperative languages that chose cooperative scheduling primarily because they truly thought the programming model is better; it's lack of composability with imperative paradigms is fairly obvious (and I know for a fact that C#, Kotlin, C++, Rust, and JS didn't choose that style because of that, but rather because of other constraints).

[1]: https://en.wikipedia.org/wiki/Esterel

Re:Java 21 Released by n2d4 on Tuesday September 19, 2023 @01:43PM (#37573441)
> In the cooperative style, the correctness of any subroutine is implicitly dependent on there not being scheduling points where they're not explicit

Removing double negations, you're saying "cooperative style is bad because the runtime can't interrupt user code implicitly"? You are looking at this from the perspective of the runtime implementer (meaning yourself, thank you by the way), not the programmer. You absolutely should look at this from the perspective of the programmer too.

Preemptive multitasking gives the runtime more capabilities because you can just assume the user has to deal with anything that your implicit rescheduling causes. But users don't actually deal with that, even if they should! The havoc that concurrency bugs are causing is evidence for that!

JavaScript's concurrency model is so nice because despite lacking real parallelism, you only have to think about concurrency (race conditions, data races, deadlocks, ...) at scheduling points. Rust has an interesting model with both parallelism and cooperative async-await. Its type system requires strong guarantees to move data between (preemptively scheduled) threads, but much weaker guarantees to use it in a cooperatively scheduled async function. Of course, it comes at a cost, namely function coloring.

After decades of legacy, none of that is feasible in Java of course, but let's not pretend that preemptive scheduling is objectively "preferable".

Re:Java 21 Released by pron on Tuesday September 19, 2023 @01:52PM (#37573600)
> That's a weird way to phrase "in the cooperative style, scheduling points are explicit, and in the preemptive style, the burden is on the developer to get it right".

Because it's the opposite. In the preemptive style, the mutual exclusion assumption -- i.e. the relevant correctness assumption -- is explicit, while in the cooperative style the correctness assumption is implicit and does not compose well. I.e. any addition of a scheduling point requires re-examining the implicit assumptions in all transitive callers.

> Preemptive multitasking gives the runtime more capabilities and makes it easier to implement because you can just assume the user has to deal with anything that your implicit rescheduling causes.

Preemptive multitasking is far, far harder to implement (that's why virtual threads took so long) and it provides superior composability. Cooperative scheduling is very easy to implement (often all it requires is just a change to the frontend compiler, which is why languages that have no support of the backend have to choose it) but it places the burden of examining implicit assumptions on the user.

> After decades of legacy, none of that is feasible in Java of course, but let's not pretend that preemptive scheduling is objectively "preferable".

Java is no different in this case from C# or Rust. They all support threading. But after significant research we've decided to spend years on offering the better approach. It looks like C# has now realised that, while hard, it may be possible for them, too, so they're giving it a go.

If we look at true clean-slate choices, the two languages that are most identified with concurrency as their core -- Erlang and Go -- have also chosen preemptive scheduling. Languages that didn't have either done so due to legacy constraints (JS), lack of control over the backend (Kotlin), complex/expensive allocation in low-level languages (C++ and Rust), or possibly because it's simply much easier (C# I guess).

Re:Java 21 Released by n2d4 on Tuesday September 19, 2023 @02:34PM (#37574171)
You don't have to transitively examine callers because you have colored functions. Every awaited call is a scheduling point. You can then still use the mutexes provided by those languages.

The "easier to implement" part was a remnant of an older rewording, and I edited it out immediately after posting. You must've seen the comment right after I posted it, sorry about that, that wasn't supposed to be my point.

Either way, we could argue here for days, but the difference in opinions here on this thread AND among programming language maintainers are enough evidence to show that the decision isn't as clear as you make it out to be. It's a little dismissive to pretend that other language designers, eg. those of Python, Kotlin, haven't done their years of research, and made inferior choices due to lack of expertise, when a LOT of developers (eg. myself) like those languages particularly because of those decisions.

Opinions differ. And if you do research on Java developers you'll get different results than if you do research on JS developers. For good reasons. I fully trust you that you made the right decision for Java.

Re:Java 21 Released by pron on Tuesday September 19, 2023 @03:02PM (#37574702)
> You don't have to transitively examine callers because you have colored functions.

You do, because the problem is not the "await" calls but everything else. The language tells you which are the callers that need to be changed when you a scheduling point, but it doesn't tell you whether that change preserves their correctness or not. You have to analyse every caller individually to know whether there was a reliance on mutual exclusion or not.

> It's a little dismissive to pretend that other language designers, eg. those of Python, Kotlin, haven't done their years of research, and made inferior choices due to lack of expertise,

But I said the opposite: that they chose the best option that was available to them. Kotlin couldn't implement user-mode threads, or at least not efficiently, because they have no control over the platforms they run on. JS also had little choice because adding threads would have broken a lot of existing code. In their place I would have made the same choice. But the point is that in imperative languages the choice of cooperative scheduling is not done because the programming model it offers is attractive but because of other constraints. All else being equal, cooperative scheduling offers worse composition in imperative languages; it's just that in many cases all else is not equal and there are other constraints.

But yes, the important thing is that even though cooperative scheduling would have been so much easier to provide in Java, we decided to take our time and do what offers the best experience to Java developers.

Re:Java 21 Released by kaba0 on Tuesday September 19, 2023 @05:12PM (#37576761)
> It's a little dismissive to pretend that other language designers, eg. those of Python, Kotlin, haven't done their years of research

Different features have different levels of depth in languages - as mentioned, where it is only a frontend compiler level change, it definitely didn’t require as much background knowledge as Loom.

Hell, Ron has been working on this exact problem for close to a decade, if I’m not mistaken! Kotlin is barely a decade old!

Re:Java 21 Released by ulrikrasmussen on Tuesday September 19, 2023 @01:22PM (#37573172)
That is a conscious design choice by the language designers. They feel that having to "color" your code as either async or sync is tedious leads to code duplication. After having worked a lot with Kotlin coroutines, I tend to agree.