Readit News logoReadit News
ljackman · 5 years ago
`Async`/`await` or something like Kotlin's `suspend` are great language features for certain domains in which a developer needs to manage blocking system calls: in lower-level languages such as Rust or C, you probably don't want to pay for a lightweight "task runtime" Like Go's or Erlang's. They bring not only a scheduling overhead but also FFI complications.

However, for application languages that can afford a few extra nicities like garbage collection, I fail to understand why the stackless coroutine model (`suspend` in Kotlin) or `async`/`await` continue to be the developer's choice. Why do languages like Kotlin adopt these features, specifically?

Manually deciding where to yield in order to avoid blocking a kernel thread seems outside of the domain of problems that those using a _higher level_ language want to solve, surely?

The caller should decide whether to do something "in the background". And this applies to non-IO capabilities too, as sometimes pure computations are also expensive enough to warrant not blocking the current task.

Go and Erlang seem to have nailed this, so I'm glad Java is following in their footsteps rather than the more questionnable strategy of C# and Kotlin. (Lua's coroutines and Scheme's `call-with-current-continuation` deserve an honourable mention too.)

geodel · 5 years ago
Kotlin runs on JVM so if JVM does not support something natively Kotlin can't have that feature like task runtime.
jpgvm · 5 years ago
Additionally Kotlin also targets native. `suspend` in Kotlin was very much designed with this in mind as it's easier to implement than something that requires an extensive runtime like Loom.

Kotlin will still support Loom on JVM and there will likely be integration with suspend/flows etc also.

jnwatson · 5 years ago
I have a lot of experience using concurrency in Go, and for the last couple years have been at the bleeding edge of Python async. The tradeoffs between the two approaches are immense.

With the virtual thread model you have:

* No function coloring problem. This also means existing code is easier to port.

* possibility of transparent M:N scheduling.

* Impedence mismatch with OS primitives.

* Much more sophisticated runtime.

* Problematic task cancellation.

* Lots of care still needed for non-trivial inter-task synchronization.

With the async API model you have:

* Viral asyncification (the method color problem).

* Simpler runtime.

* Obvious and safe task cancellation.

* Completely orthogonal to parallelism (actually doing more than one thing simultaneously) for good and for bad.

* Inter-task coordination is straightforward and low-overhead even for sophisticated use cases.

* Higher initial learning curve.

I'm leaning toward liking the async approach more, but that might be just because I'm deep in the middle of it. I think the biggest argument in favor of virtual threads is the automatic parallelism; that's also the biggest argument against: free running threads require more expensive synchronization and introduce nondeterminism.

pron · 5 years ago
* Java offers both user-mode and kernel threads. You pick at creation, and can even plug your own scheduler.

* Loom's virtual threads are completely scheduled in library code, written in Java.

* FFI that bypasses the JDK and interacts with native code that does either IO or OS-thread synchronization is extremely rare in Java.

* Cancellation is the same for both.

Also, IMO, coordination is simpler for threads than for async. Where they differ is in their choice of defaults: thread allow scheduling points anywhere except where explicitly excluded; async/await allows scheduling points nowhere except where explicitly allowed. Putting aside that some languages have both, resulting in few if any guarantees, threads' defaults are better for correct concurrency. The reason is that correctness relies on atomicity, or lack of scheduling points in critical sections. When you explicitly exclude them, none of your callees can break your correctness. When you explicitly allow them, any callee can become async and break its caller's logic. True, the type system will show you where the relevant callsites are, but it will not show you whether there is a reliance on atomicity or not.

Async/await does, however, make sense for JavaScript, where all existing code already has an implicit assumption of atomicity, so breaking it would have broken the world. For languages that have both, async/await mostly adds a lot of complexity, although sometimes it is needed for implementation reasons.

jayd16 · 5 years ago
One headache I see that no one seems to mention is thread affinity seems a lot harder to manage in an implicit system. Many patterns use a single thread for synchronization but often times one thread is special. UI systems often have a UI thread that controls the GLContext or what have you. In something like C#'s async, you can easily schedule tasks off and on that thread. I'm not sure how you could do this implicitly. Loom seems to keep around native Threads for this sort of thing?

I would be really interested in seeing a UI system written with Loom.

>FFI that bypasses the JDK and interacts with native code that does either IO or OS-thread synchronization is extremely rare in Java.

I would also argue its rare because its hard to do. This is a self fulfilling argument. In C#, a very similar language, its much more common to do that sort of thing because its easier. C# is chosen for games and other apps because it can interop with native more easily.

ryanworl · 5 years ago
Is it possible to override the scheduler from user code? i.e. if you wanted to control scheduling yourself for some reason.
jerf · 5 years ago
I don't think "task cancellation" is quite the major difference you think. If you model it as thread A wants to cancel thread B, then while threading means that A runs and cancels B, but B may need some time to catch up, the async world has the problem of thread A running at all to cancel B, if B is having a problem that requires cancellation. It's "obvious" and "safe" until it doesn't happen at all.

This is a pervasive problem with the async/await model. As it scales up the probability of something failing to yield when it should and blocking everything else continually goes up as the code size increases, and then the whole model, correctness, practicality, and all, just goes out the window. While it is small for small programs, and it the scaling factor often isn't that large, it is still a thing that happens. Entire OSes used to work that way, with the OS and user processes cooperatively yielding, and what killed that model is this problem.

Also, I'm writing a lot of code lately where I can peg multiple cores at a time, with a relatively (if not maximally efficient) language like Go; having to also write it as a whole bunch of OS processes separately running because my runtime can only run on one core at a time is a non-starter, and "async/await" basically turns into a threading system if you try to run it on multiple cores in one system anyhow.

These two fatal-for-me flaws mean it's a non-starter for a lot of the work I'm doing anyhow, regardless of any other putative advantages.

(As I mentioned, I'm using Go, but if you want to see a runtime that really has the asynchronous exceptions thing figured out, go look at Erlang. Having a thread run off into never-never-land and eating a full CPU isn't fun, but being able to log in to your running system, figure out which it is using a REPL, kill just that thread, and reload its code before restarting it to fix the problem, all without taking down the rest of your system is not an experience most of you have had. But it can be done!)

jnwatson · 5 years ago
Async, and cooperative multitasking in general, requires all members participate in the contract: you shall not block and you shall not go too long until yielding. Once a piece of code violates that, all bets are off. Python has explicit debugging mechanisms to help a developer detect the latter.

Cancel safety is less about killing an out-of-control task, and more about making sure the state after cancelling a task is consistent.

dnautics · 5 years ago
> but if you want to see a runtime that really has the asynchronous exceptions thing figured out, go look at Erlang.

The erlang VM does indeed have async exceptions, and resource management figured out. Usually you can just kill an erlang process and you don't have to clean up after it's open sockets, file descriptors, etc.

It's also possible to hook the C FFI system to take full advantage of that: https://youtu.be/l848TOmI6LI

(Disclaimer: self promotion)

earthboundkid · 5 years ago
Cancellation is a little tricky. If things can cancel at any point, then it's impossible to write safe/correct code. Async has the advantage that the await calls are natural sync points, so it's (probably) safe to cancel there. But historical experience has also shown that people will forget to yield when they should, and it will lead to cooperation problems.

I think the best approach has to look something like Go's, but perhaps a bit more structured (dynamic scoping[1] might help perhaps with task nurseries[2]). Unless you're writing extremely low level code, you want your language runtime to intercept all syscalls and figure out the async story for you. The language should handle making sure that the M:N mapping works out, no one opens a socket the wrong way etc. Then for you as the program writer, your responsibility is just setting explicit cancellation points as part of the general error handling approach. It's still not perfect, but I think that would be the next evolution from what exists today.

[1] https://blog.merovius.de/2017/08/14/why-context-value-matter...

[2] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...

jayd16 · 5 years ago
If Loom isn't adding cancellation tokens to the core library then its going to be a major difference. That said, the Java way was already for runnables to cancel themselves with some kind of volatile cancel bool so I expect that's all we'll get.

From the article: >Clearly the intent is also to cancel the child thread. And this is exactly what happens.

I find this to be a bold choice. In C# you can detach children and such. Should be interesting to see if this gets added later.

throwaway894345 · 5 years ago
I've had similar experiences, but I don't much care for async Python. In particular, it's way too easy to block the event loop either by accidentally calling some function that, perhaps transitively, does blocking I/O (this could be remedied if there was no sync IO) or simply by calling a function which is unexpectedly CPU-bound. And when this happens, other requests start failing unrelated to the request that is causing the problem, so you go on this wild goose chase to debug. Sync I/O is also a much nicer, more ergonomic interface than async IMO. And then there are the type error problems--it's way too easy to forget to `await` something. Mypy could help with this, but it's still very, very immature. Lastly, last I checked the debugger couldn't cope with async syntax--this is obviously not criticizing the async approach in general, but I wanted to round out my complaining about async Python.

I don't mind working with goroutines personally--I use them sparingly, only when I really need concurrency or parallelism. This takes some discipline (e.g., not to go crazy with goroutines and/or channels) and a bit of experience (in the presence of multiple goroutines, what needs to be locked, when to use channels, etc), so if you're relatively new and very impatient or undisciplined you probably won't have a good time (which isn't to say that if you dislike goroutines you must be a novice or undisciplined!). But for me it's nearly an ideal experience.

breatheoften · 5 years ago
I'm not sure I really think of function coloring as a "problem" ...

facebook is experimenting with auto differentiation for Kotlin and looks like it's adding a new "differentiable" function color -- https://ai.facebook.com/blog/paving-the-way-for-software-20-...

It looks very easy to reason about and use to me ... and i personally find async a similarly useful marker ... It's about being able to push constraints from caller arbitrarily far down the callee stack -- which is really not something that types support at all but provides for a very high confidence variety of constraint -- and high confidence constraints seem to me like they convey a ton of information.

I've been wondering actually whether "function colors" might actually just be a good way to create a whole variety of strong statically enforceable constraints for functions. It seems like they lead to very good and simple programmer mental models ...

Are there languages that offer "user definable" function colors? I can think of a lot of application domains that would be much better served by these kinds of constraints than oo or other type-centric approaches ... it would be ridiculously useful to be able to mark a function with the "MyDomainBusinessLogic" color and get assurances that such a method can only call other functions annotated with that color ... would provide an easy way to iterate on app specific abstractions provide compiler assistance for the communication of layering intent -- rather than a bunch of poorly specified words in documents that try to communicate layering intent to other developers -- in language that is either sufficiently precise as to be incomprehensible -- or sufficiently vague as to be subject to (mis)interpretation ...

jdlshore · 5 years ago
There was a series of essays about esoteric language features posted here a few weeks ago, and one of them was exactly what you're talking about. It was a functional language, with the ability to mark functions as involving I/O, non-terminating, or (I think) arbitrary custom "colors."

It was an academic language, but very interesting. Sadly, I don't remember the name. Maybe somebody else can post the link.

svieira · 5 years ago
A couple of excellent articles on the different ways of thinking about the choice (of when to yield) vs. color (virality of choice) problem:

* Choice is bad: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... * Choice is good: https://glyph.twistedmatrix.com/2014/02/unyielding.html

pron · 5 years ago
> It does nothing for you if you have computationally intensive tasks and want to keep all processor cores busy.

I would argue this isn't concurrency at all (the job of juggling mostly independent tasks, and scheduling them to a relatively small number of processing units), but parallelism (the job of performing a single computational task faster by employing multiple processing units), and exactly the job of parallel streams.

> It doesn’t help you with user interfaces that use a single event thread.

It might. Loom allows you to plug in your own scheduler, and it is a one-liner to schedule virtual threads on the UI thread:

    var uiVirtualThreadFactory = Thread.builder().virtual(java.awt.EventQueue::invokeLater).factory();
All threads created by this factory will be virtual threads that are always "carried" by the UI OS thread. This won't currently work because each of those threads will have its own identity, and various tests in the UI code check that the current thread is actually the UI thread and not any thread that is mapped to the same OS thread. Changing these tests is something the UI team is looking into.

jayd16 · 5 years ago
>All threads created by this factory will be virtual threads that are always "carried" by the UI OS thread

Does this actually solve the problem? I don't see it. We want to interweave foreground and background work. Sometimes that means blocking work will yield, sometimes that means it should not yield because conceptually several tasks should retain exclusive control of that thread. You might want some IO task on the background but you need a block of OpenGL tasks to retain control.

I just don't see how you can do this implicitly in a way that's cleaner than async/await. It seems like posting tasks to this thread factory or that will get the job done but is that an improvement?

It sounds like for now this stuff will still be using the current model of posting unyielding runnables to a thread. That's fine I guess. Loom still seems very cool, it just doesn't cover the cases I deal with a lot more often.

pron · 5 years ago
It does (or will do, once the checks in the UI code are fixed) exactly what you want it to do. All computation will be done on the UI OS thread. All blocking operations will release it to do other things so it remains responsive. If you want to compute something outside it -- just spawn another thread that's not mapped to it.
mping · 5 years ago
For me the real advantage is not on performance but on the programming model. I have been tinkering with Loom (and clojure) and the idea of "just" calling some library without worrying about blocking is refreshing. That means that for the most of it, you can write your code without worrying too much about some kind of callbacks or async support from your library and it just works.

Of course, for those with extreme performance requirements, they will probably have their own custom scheduler and concurrency/parallelism mechanisms but for the vast majority of jvm users out there I think Loom will be a great thing. If Loom integrates with GraalVM/native-image it would be even nicer.

lackbeard · 5 years ago
I think the vast majority of JVM users won't even need Loom. OS threads perform well enough for most use cases. You can go a very long way with just a ThreadPoolExecutor.
closeparen · 5 years ago
No one “needs” Loom; you can always write in callback oriented style. The point is it will free you from that.
kasperni · 5 years ago
For those that care about numbers: Loom targets ~ 200b memory overhead per virtual thread. And ~ 100ns per context switch between virtual threads.
nfoz · 5 years ago
The article seems to assume you know what Project Loom is. (Not to be confused with Google's Project Loon, the balloon thing.)

From https://wiki.openjdk.java.net/display/loom/Main, it's an OpenJDK project:

> Project Loom is to intended to explore, incubate and deliver Java VM features and APIs built on top of them for the purpose of supporting easy-to-use, high-throughput lightweight concurrency and new programming models on the Java platform.

A bit more history/explanation here:

http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.h...

pbourke · 5 years ago
To summarize: up till now, Java Threads have been 1:1 with OS threads. They’re limited to a few thousand per JVM. This project moves to an M:N threading model but retains the Thread API. It allows for millions of threads per JVM and async/await style performance of the existing synchronous Java libraries without language changes and with minimal changes to the standard library.
gpderetta · 5 years ago
Originally (about 20 years ago) java threads were M:N I think. How is this different? If I had to guess, they are not opaque to the VM which has more freedom to optimize them.
dboreham · 5 years ago
There was a before time when they weren't 1:1
technologia · 5 years ago
Thank you, I totally clicked on this thinking how could Google Loon have anything to do with this
moks · 5 years ago
dangerboysteve · 5 years ago
timley

Episode 8 “Project Loom” with Ron Pressler

https://inside.java/2020/11/24/podcast-008/

Deleted Comment