Hacker News new | ask | show | jobs
by littlestymaar 2424 days ago
> So I certainly don't think async/await is wrong for C++/Rust/Zig, but I think it's wrong for, say, JavaScript and C#, and we're going a different way in Java (more like Scheme's).

How can it be wrong for JavaScript? Because JavaScript is a single threaded environment, it was the only possible choice and it's one of the most influential change in the language (in itself it changed more the language use than the whole ES6 bundle). It's a really great success and I'm not sure why you'd like to revert it.

1 comments

> I'm not sure why you'd like to revert it.

I don't. The old way and async/await aren't the only two options.

> it was the only possible choice

Why?

What would you do instead? Use a green thread system on top of a single-threaded VM? Great, now you have two kinds of blocking calls: the one which only block its own thread, and the one which block all threads at the same time because it blocks the VM itself. How ergonomic!

Remember, the single threaded character of the js VM is not an implementation detail, it's part of the spec. Hate it or love it but the web works this way.

Also, if you think threads are a good concurrency abstraction, let's play a little game: consider you need to read 1M files on a spinning disk. How many threads do you need to run on to get the maximum read performance:

a) one per file

b) just one

c) one per core

d) a magic number which depends on your hard disk's firmware and your workload.

Convenient and intuitive right?

Threads are a dated concurrency primitive which would have died long ago if wasn't also a good parallelism primitive.

> Great, now you have two kinds of blocking calls: the one which only block its own thread, and the one which block all threads at the same time because it blocks the VM itself. How ergonomic!

You could only have the first kind, but it's the same situation with async/await. Only async/await tracks the "good" kind of blocking, yet lets the "bad" kind go untracked.

> How many threads do you need to run on yo get the maximum read performance

The exact same number as you would for doing it with `await Promise.all` The same knowledge you have about the scheduling mechanism doesn't go away if you're no longer required to annotate functions with `async`.

> Threads are a dated concurrency primitive which would have died long ago if wasn't also a good parallelism primitive.

Maybe they are, but async/await are the exact same construct only that you have to annotate every blocking function with "async" and every blocking call with "await". If you had a language with threads but no async/await that had that requirement you would not have been able to tell the difference between it and one that has async/await.

> Only async/await tracks the "good" kind of blocking, yet lets the "bad" kind go untracked.

The “bad kind” is indistinguishable from CPU intensive computation anyway (which cannot be tracked), but at least you have a guarantee when you are using the good kind. (Unfortunately, in JavaScript, promises are run as soon as you spawn them, so they can still contain a CPU heavy task that will block your event loop, Rust made the right call by not doing anything until the future is polled).

> The exact same number as you would for doing it with `await Promise.all`

From a user's perspective, when I'm using promises, I have no idea how it's run behind (at it can be nonblocking all the way down if you are using a kernel that supports nonblocking file IOs). This example was specifically about OS threads though, not about green ones (but it will still be less expensive to spawn 1M futures than 1M stackful coroutines).

> Maybe they are, but async/await are the exact same construct only that you have to annotate every blocking function with "async" and every blocking call with "await". If you had a language with threads but no async/await that had that requirement you would not have been able to tell the difference between it and one that has async/awaitof

I don't really understand your point. Async/await is syntax sugar on top of futures/promises, which itself is a concurrency tool on top of nonblocking syscalls. Of course you could add the same sugar on top of OS threads (this is even a classic exercise for people learning how the Future system works in Rust), that wouldn't make much sense to use such thing in practice though.

The question is whether the (green) threading model is a better abstraction on top of nonblocking syscalls than async/await is. For JavaScript the answer is obviously no, because all you have behind is a single threaded VM, so you lose the only winning point of green threading: the ability to use the same paradigm for concurrency and parallelism. In all other regards (performance, complexity from the user's perspective, from an implementation perspective, etc.) async/await is just a better option.

> which cannot be tracked

Of course it can be tracked. It's all a matter of choice, and things you've grown used to vs. not.

> but at least you have a guarantee when you are using the good kind

Guarantee of what? If you're talking about a guarantee that the event loop's kernel thread is never blocked, then there's another way of guaranteeing that: simply making sure that all IO calls use your concurrency mechanism. As no annotations are needed, it's a backward-compatible change. That's what we're trying to do in Java.

> but it will still be less expensive to spawn 1M futures than 1M stackful coroutines.

It would be exactly as expensive. The JS runtime could produce the exact same code as it does for async/await now without requiring async/await annotations.

> Async/await is syntax sugar on top of futures/promises, which itself is a concurrency tool on top of nonblocking syscalls.

You can say the exact same thing about threads (if you don't couple them with a particular implementation by the kernel), or, more precisely, delimited continuations, which are threads minus the scheduler. You've just grown accustomed to thinking about a particular implementation of threads.

> The question is whether the (green) threading model is a better abstraction on top of nonblocking syscalls than async/await is

That's not the question because both are the same abstraction: subroutines that block waiting for something, and then are resumed when that task completes. The question is whether you should make marking blocking methods and calls mandatory.

> In all other regards (performance, complexity from the user's perspective, from an implementation perspective, etc.) async/await is just a better option.

The only thing async/await does is force you to annotate blocking methods and calls. For better or worse, it has no other impact. A clear virtue of the approach is that it's the easiest for the language implementors to do, because if you have those annotations, you can do the entire implementation in the frontend; if you don't want the annotation, the implementors need to work harder.

Of course, you could argue that you personally like the annotation requirement and that you think forcing the programmer to annotate methods and calls that do something that is really indistinguishable from other things is somehow less "complex" than not, but I would argue the opposite.

I have been programming for about thirty years now, and have written extensively about the mathematical semantics of computer programs (https://pron.github.io/). I understand why a language like Haskell needs an IO type (although there are alternatives there as well), because that's one way to introduce nondeterminism to an otherwise deterministic model (I discuss that issue, as well as an alternative -- linear types -- plus async/await and continuations here: https://youtu.be/9vupFNsND6o). And yet, no one can give me an explanation as to why one subroutine that reads from a socket does not require an `async` while another one does even though they both have the exact same semantics (and the programming model is nondeterministic anyway). The only explanation invariably boils down to a certain implementation detail.

That is why I find the claim that even when two subroutines have the same program semantics, and yet the fact that they differ in an underlying implementation detail means that they should have a different syntactic representation, is somehow less complex than having a single syntactic representation to be very tenuous. Surfacing implementation details to the syntax level is the very opposite of abstraction and the very essence of accidental complexity.

Now, I don't know JS well, and there could be some backward compatibility arguments (e.g. having to do with promises maybe), but that's a very different claim from "it's less complex", which I can see no justification for.

There are two different things: semantic and syntax.

From what I understand now, you are arguing about syntax: we should not need to write “async” or “await”. I'm not really going to discuss this, because as you said, I do like the extra verbosity and I actually like explicit typing for the same reason (Rust is my favorite, with just the right level of inference) and I'm not fond of dynamic typing or full type inference. This is a personal taste and that isn't worth arguing about.

On the other hand, there is also a semantic issue, and sorry I have to disagree, stack-ful and stack-less coroutines don't have the same semantic, they don't have the same performance characteristic nor they do have the same expressiveness (and associated complexity, for users and implementers). What I was arguing that if you want the full power of threads, you pay the price for it.

But from what I now understand, you just want a stackless coroutine system without the extra async/await” keywords, is that what you mean?

> > [CPU intensive computation,] which cannot be tracked

> Of course it can be tracked. It's all a matter of choice, and things you've grown used to vs. not.

What? When has the halting problem become “something you've grown used to”?!