6 ms·
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.
by lambda_garden 3y ago
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?
- pjmlp 3y agoThey also have enough gotchas that one of the main ASP.NET architects had to write a best practices document to address all common misuses.
- neonsunset 3y agoSome 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.
- kaba0 3y agoStating something doesn’t make it so.
- wiseowise 3y agoThat’s the idea behind suspend keyword in Kotlin: https://elizarov.medium.com/how-do-you-color-your-functions-a6bb423d936d https://elizarov.medium.com/how-do-you-color-your-functions-...
- djur 3y agoWhy do you like that? What problems has it helped you solve or avoid?
- lolinder 3y agoNot 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.
- hocuspocus 3y agoYes 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.
- deleted 3y ago[deleted]
- pgwhalen 3y agoIt 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.
- lolinder 3y agoIt'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.
- lolinder 3y agoI 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-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
- lambda_garden 3y agoJava 21 is colorblind - but the colors still exist
- lolinder 3y agoYep, 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.
- kaba0 3y agoThis 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?
- kaba0 3y agoStructured 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.
- lern_too_spel 3y agoWhy should that matter? If you want to run two tasks concurrently, you run them concurrently the same way, whether they are blocking or not.
- mike_hearn 3y agoI'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.
- gary_clark 3y agoVirtual threads don't give you async IO, they give you (the illusion of) more threads. If one VT blocks, the underlying OS thread switches to another VT. All IO is blocking.
- jayd16 3y agoIt'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.
- lbalazscs 3y agoThe 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.
- jayd16 3y agoI 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.
- lbalazscs 3y agoIdeally 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.
- lolinder 3y agoAsync 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.
- kaba0 3y agoI’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.
- aardvark179 3y agoIn 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.
- svieira 3y agoThis is an old discussion, Unyielding (https://glyph.twistedmatrix.com/2014/02/unyielding.html 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-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.
- pron 3y agoYes. 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.
- lolinder 3y ago> 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.
- pron 3y ago
- ulrikrasmussen 3y agoThat 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.