Hacker News new | ask | show | jobs
by nlitened 695 days ago
> is far easier for most people

I’d say that writing single-threaded code is far easier for _all_ people, even async code experts :)

Also, single-threaded code is supported by programming language facilities: you have a proper call stack, thread-local vars, exceptions bubbling up, structured concurrency, simple resource management (RAII, try-with-resources, defer). Easy to reason and debug on language level.

Async runtimes are always complicated, filled with leaky abstractions, it’s like another language that one has to learn in addition, but with a less thought-out, ad-hoc design. Difficult to reason and debug, especially in edge cases

2 comments

> Async runtimes are always complicated, filled with leaky abstractions, it’s like another language that one has to learn in addition, but with a less thought-out, ad-hoc design. Difficult to reason and debug, especially in edge cases

Async runtimes themselves are simply attempts to bolt-on green threads on top of a language that doesn't support them on a language level. In JavaScript, async/await uses Promises to enable callback-code to interact with key language features like try/catch, for/while/break, return, etc. In Python, async/await is just syntax sugar for coroutines, which are again just syntax sugar for CPS-style classes with methods split at each "yield". Not sure about Rust, but it probably also uses some Rust macro magic to do something similar.

Indeed. Async runtimes/sytles are attempts to provide a more readable/useable syntax for CPS[1]. CPS originally had nothing to do with blocking/non-blocking or multi-threading but arose as a technique to structure compiler code.

Its attraction for non-blocking coding is that it allows hiding the multi-threaded event dispatching loop. But as the parent comment suggests, this abstraction is extremely leaky. And in addition, CPS in non-functional languages or without syntactic sugar has poor readability. Improving the readability requires compiler changes in the host language - so many languages have added compiler support to further hide the CPS underpinnings of their async model.

I've always felt this was a big mistake in our industry - all this effort not only in compilers but also in debuggers/IDE - building on a leaky abstraction. Adding more layers of leaky abstractions has only made the issue worse. Async code, at first glance, looks simple but is a minefield for inexperienced/non-professional software engineers.

It's annoying that Rust switched to async style - the abstraction leakiness immediately hits you, as the "hidden event dispatching loop" remains a real dependency even if it's not explicit in the code. Thus libraries using asycn cannot generally be used together although last time i looked, tokio seems to have become the de-facto standard.

[1] https://en.wikipedia.org/wiki/Continuation-passing_style

I absolutely agree that the virtual/green thread style is much better, more ergonomic, less likely to be correct, etc, but I can’t fault Rust’s choice, given it being a low-level language without a fat runtime, making it possible to be called into from other runtimes. What the JVM does is simply not possible that way.
>Async runtimes themselves are simply attempts to bolt-on green threads on top of a language that doesn't support them on a language level.

Haskell supports async code while also supporting green threads on a language level, and the async code has most of the same issues as async code in any other languages.

What problems exactly? Haskell has a few things that imo it does better than most languages in this area:

- All IO is non-blocking by default.

- FFI support for interruptible.

- Haskell threads can be preempted externally - this allows you to ensure they never leak. Vs a goroutine that can just spin forever if it doesn't explicitly yield.

- There are various stdlib abstractions for building concurrent programs in a compositional way.

> Haskell threads can be preempted externally - this allows you to ensure they never leak. Vs a goroutine that can just spin forever if it doesn't explicitly yield.

Goroutines are preemptible by the runtime (since https://go.dev/doc/go1.14#runtime) but they're still not addressable or killable through the language itself.

The GHC runtime has lots of cool concurrency features.

Async exceptions as a way to pass messages (and kill threads!)

Allocation limits for threads.

Software Transactional Memory.

> Not sure about Rust, but it probably also uses some Rust macro magic to do something similar.

Much the same as JavaScript I understand, but no macros; the compiler turns them into Futures that can be polled

>I’d say that writing single-threaded code is far easier for _all_ people, even async code experts :)

While 'async' is just a name, underneath it's epoll - and the virtual threads would not perform better than a proper NIO (epoll) server. I dont consider myself an 'async expert' but I have my share of writing NIO code (dare say not terrible at all)

Virtual threads literally replace the “blocking” IO call issued by the user, by a proper NIO call, mounting the issuer virtual thread when it signals.