Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Async is a Javascript hack that inexplicably got ported to other languages that didn't need it.

The issue arose because Javascript didn't have threads, and processing events from the DOM is naturally event driven. To be fair, it's a rare person who can deal with the concurrency issues threads introduce, but the separate stacks threads provide a huge boon. They allow you to turn event driven code into sequential code.

    window.on_keydown(foo);

    // Somewhere far away
    function foo(char_event) { process_the_character(char_event.key_pressed) };
becomes:

    while (char = read())
        process_the_character(char);
The latter is easy to read linear sequence of code that keeps all the concerns in one place, the former rapidly becomes a huge entangled mess of event processing functions.

The history of Javascript described in the article is just a series of attempts to replace the horror of event driven code with something that looks like the sequential code found in a normal program. At any step in that sequence, the language could have introduced green threads and the job would have been done. And it would have been done without new syntax and without function colouring. But if you keep refining the original hacks they were using in the early days and don't the somewhat drastic stop of introducing a new concept to solve the problem (separate stacks), you end up where they did - at async and await. Mind you, async and await to create a separate stack of sorts - but it's implemented as a chain objects malloc'ed on the heap instead the much more efficient stack structure.

I can see how the javascript community fell into that trap - it's the boiling frog scenario. But Python? Python already had threads - and had the examples of Go and Erlang to show how well then worked compared to async / await. And as for Rust - that's beyond inexplicable. Rust has green threads in the early days and abandoned them in favour of async / await. Granted the original green thread implementation needed a bit of refinement - making every low level choose between event driven and blocking on every invocation was a mistake. Rust now has a green thread implementation that fixes that mistake, which demonstrates it wasn't that hard to do. Yet they didn't do it at the time.

It sounds like Zig with its pluggable I/O interface finally got it right - they injected I/O as a dependency injected at compile time. No "coloured" async keywords and compiler monomorphises the right code. Every library using I/O only has to be written once - what a novel concept! It's a pity it didn't happen in Rust.



async/await came out of C# (well at least the JS version of it).

There are a bunch of use cases for it outside of implementing concurrency in a single threaded runtime.

Pretty much every GUI toolkit I've ever used was single threaded event loop/GUI updates.

Green threads are a very controversial design choice that even JVM backed out of.


Yep and I loved when C# introduced it. I worked on a system in C# that predated async/await and had to use callbacks to make the asynchronous code work. It was a mess of overnested code and poor exception handling, since once the code did asynchronous work the call stack became disconnected from where the try-catches could take care of them. async/await allowed me to easily make the code read and function like equivalent synchronous code.


> async/await came out of C# (well at least the JS version of it).

Not sure if inspired by it, but async/await is just like Haskells do-notation, except specialized for one type: Promise/Future. A bit of a shame. Do-notation works for so many more types.

- for lists, it behaves like list-comprehensions.

- for Maybes it behaves like optional chaining.

- and much more...

All other languages pile on extra syntax sugar for that. It's really beautiful that such seemingly unrelated concepts have a common core.


I knew someone was going to bring up monads that's why I put JS version :) JS took the C# syntax.


Similarly F#'s computation expressions predate C#'s syntax, and there is some evidence that C# language designers were looking at F#'s computation expressions. Since the Linq work, C# has been very aware of Monads, and very slow and methodical about how it approaches them. Linq syntax is a subtly compromised computation expression and async/await is a similar compromise.

It's interesting to wonder about the C# world where those things were more unified.

It's also interesting to explore in C# all the existing ways that Linq syntax can be used to work with arbitrary monads and also Task<T> can be abused to use async/await syntax for arbitrary monads. (In JS, it is even easier to bend async/await to arbitrary monads given the rules of a "thenable" are real simple.)


> use async/await syntax for arbitrary monads. (In JS, it is even easier to bend async/await to arbitrary monads given the rules of a "thenable" are real simple.)

I tried once to hack list comprehensions into JS by abusing async/await. You can monkey patch `then` onto Array and define it as flatMap and IIRC you can indeed await arrays that way, but the outer async function always returns a regular Promise. You can't force it to return an instance of the patched Array type.


> Green threads are a very controversial design choice that even JVM backed out of.

Did they? Project Loom has stabilized around Java 21, no?


Virtual Threads aren't quite the same as green threads (they don't block the OS thread) and they work extremely well now.


That's interesting because I remember people talking about Rust green threads as M:N mapping, which seems to be the only difference.


They are not even remotely the same, there is no reason to compare them at all.


I stand corrected, I stopped keeping track of JVM years ago, was referring to initial green threads implementation.


> async/await came out of C# (well at least the JS version of it).

Having been instrumental in accelerating bringing async/await to JS, it definitely was the case that it came from C# and we eagerly were awaiting its arrival in JS and worked with Igalia to focus on it and make it happen across browsers more quickly so people could actually depend on it.


> And as for Rust - that's beyond inexplicable. Rust has green threads in the early days and abandoned them in favour of async / await.

There was a fair bit of time between the two, to the point I'm not sure the latter can be called much of a strong motivation for the former. Green threads were removed pre-1.0 by the end of 2014 [0], while work on async/await proper started around 2017/2018 [1].

In addition, I think the decision to remove green threads might be less inexplicable than you might otherwise expect if you consider how Rust's chosen niche changed pre-1.0. Off the top of my head no obligatory runtime and no FFI/embeddability penalties are the big ones.

> Rust now has a green thread implementation that fixes that mistake

As part of the runtime/stdlib or as a third-party library?

[0]: https://github.com/rust-lang/rust/issues/17325

[1]: https://without.boats/blog/why-async-rust/


JavaScript got async in 2017, Python in 2015, and C# in 2012. Python actually had a version of it in 2008 with Twisted's @inlineCallbacks decorator - you used yield instead of await, but the semantics were basically the same.


> Python already had threads

But for a long time (I think even till today despite that there is as an optional free-threaded build) CPython used Global Interpreter Lock (GIL) which paradoxically makes the programs run slower when more threads are used. It's a bad idea to allow to share all the data structure across threads in high level safe programming languages.

JS's solution is much better, it has worker threads with message passing mechanisms (copying data with structuredClone) and shared array buffers (plain integer arrays) with atomic operation support. This is one of the reasons why JavaScript hasn't suffered the performance penalty as much as Python has.


> And as for Rust - that's beyond inexplicable.

No, you appear to have no idea what you're talking about here. Rust abandoned green threads for good reason, and no, the problems were not minor but fundamental, and had to do with C interoperability, which Go sacrifices upon the altar (which is a fine choice to make in the context of Go, but not in the context of Rust). And no, Rust does not today have a green thread implementation. Furthermore, Rust's async design is dramatically different from Javascript, while it certainly supports typical back-end networking uses it's designed to be suitable for embedded contexts/freestanding contexts to enable concurrency even on systems where threads do not exist, of which the Embassy executor is a realization: https://embassy.dev/


> the problems were not minor but fundamental, and had to do with C interoperability,

The interoperability problems came from a design choice they made: they wanted to invisibly swap between OS threads if a blocking call was made. That was the wrong design choice for Rust. It is not something green threads require - it's an additional burden they imposed on themselves.

> a fine choice to make in the context of Go,

Correct. It was.

> enable concurrency even on systems where threads do not exist,

And that was their mistake. Green threads are just user space stacks, which any CPU that has a stack pointer can support. They don't need OS threads. But in the initial Rust implementation they added "invisibly supports blocking calls" implementation. That does indeed need full OS thread support, and breaks the C interface.

They could have just abandoned that mistake, implemented pure green threads and they would have been done. But no, they instead they implemented async. So instead we got new keywords, had to wait years for various new language features to stabilise (like pin) and lifetime 'static. I'm sure the language designers spent many happy man months solving the problem "How do we represent a suspended function as a type-safe State Machine?".

What we got for all that effort is something so famously difficult to use, even experienced Rust programmer shy away from async. Whereas with green threads a programmer could just reuse their knowledge and mechanisms Rust has for sharing data across multiple stacks, for async they have now to battle the borrow checker on a new front - the language to absorb their function state into 'static area that has fewer lifetime guarantees. The alternate green thread implementation could have had just two colours - blocking and non-blocking, now we have a colour for every async library. Whereas with green threads they could have stabilised the interface to underlying the event loop, we are now stuck with every library writer having their I/O in their own mini API, which they swap for each async implementation.

And while the type-safe State Machine they did come up with is an engineering marvel, it's very complex and requires data copies under the hood. Green threads just reuse an existing mechanism for saving a function's state - the CPU's IP Address, the registers and the stack. It's so simple anyone can understand it, it isn't something additional to learn because the program's main already uses it, it is very, very fast because it's undergone decades of refinement, and it's also type safe!

But instead in what we got the syntax is a mess, the error messages are incomprehensible, and we re-implemented the CPU's stack management in software (invariably slower) because they didn't want to write the logic to grow a hardware stack.

The language will be better off recognising it was all a huge mistake, and moving towards standardising on a green thread implementation like mioco. Everyone who has to manage 100,000 lightweight processes would be immensely grateful for the simpler, faster API. Some of the truly impressive stuff done for async, like knowing how much stack some function calls can take, would be really useful for green threads. You know the size of the stack you need - to the byte! So it's not a complete loss.


> The interoperability problems came from a design choice they made: they wanted to invisibly swap between OS threads if a blocking call was made.

From what I understand the interop problems are more fundamental than that. The Rust devs wanted zero-cost FFI, but FFI for green threads would necessarily involve moving data between the green thread stack and the C stack, which is not zero-cost. Furthermore, the Rust devs wanted "zero-knowlege" FFI, in that calling code wouldn't need to know it's calling into or being called from Rust. That means that you can't assume whatever is on the other side of the FFI boundary knows how to switch/grow/otherwise handle green thread stacks, and inserting shims to do so would also not be zero-cost.

> and we re-implemented the CPU's stack management in software (invariably slower) because they didn't want to write the logic to grow a hardware stack.

If I'm understanding you correctly, part of the problem is that Rust didn't have a good mechanism to grow stacks. Segmented stacks were abandoned because of their allocations and performance cliffs, and stack copying a la Go is not feasible due to the inability to update pointers to the new larger stack. Rust ended up just using larger stacks for its green threads which kind of defeats a major reason to use green threads in the first place.


> At any step in that sequence, the language could have introduced green threads and the job would have been done.

The job wouldn’t have been done. They would have needed threads. And mutexes. And spin locks. And atomics. And semaphores. And message queues. And - in my opinion - the result would have been a much worse language.

Multithreaded code is often much harder to reason about than async code, because threads can interleave executions and threads can be preempted anywhere. Async - on the other hand - makes context switching explicit. Because JS is fundamentally single threaded, straight code (without any awaits) is guaranteed to run uninterrupted by other concurrent tasks. So you don’t need mutexes, semaphores or atomics. And no need to worry about almost all the threading bugs you get if you aren’t really careful with that stuff. (Or all the performance pitfalls, of which there are many.)

Just thinking about mutexes and semaphores gives me cold sweats. I’m glad JS went with async await. It works extremely well. Once you get it, it’s very easy to reason about. Much easier than threads.


Once you write enough code, you'll realize you need synchronization primitives for async code as well. In pretty much the same cases as threaded code.

You can't always choose to write straight code. What you're trying to do may require IO, and then that introduces concurrency, and the need for mutual exclusion or notification.

Examples: If there's a read-through cache, the cache needs some sort of lock inside of it. An async webserver might have a message queue.

The converse is also true. I've been writing some multithreaded code recently, and I don't want to or need to deal with mutexes, so, I use other patterns instead, like thread locals.

Now, for sure the async equivalents look and behave a lot better than the threaded ones. The Promise static methods (any, all, race, etc) are particularly useful. But, you could implement that for threads. I believe that this convenience difference is more due to modernity, of the threading model being, what 40, 50, 60 years old, and given a clean-ish slate to build a new model, modern language designers did better.

But it raises the idea: if we rethought OS-level preemptible concurrency today (don't call it threads!), could we modernize it and do better even than async?


> Once you write enough code, you'll realize you need synchronization primitives for async code as well. In pretty much the same cases as threaded code.

I've been programming for 30 years, including over a decade in JS. You need sync primitives in JS sometimes, but they're trivial to write in javascript because the code is run single threaded and there's no preemption.

> What you're trying to do may require IO

Its usually possible to factor your code in a way that separates business logic and IO. Then you can make your business logic all completely synchronous.

Interleaving IO and logic is a code smell.

> The Promise static methods (any, all, race, etc) are particularly useful. But, you could implement that for threads. I believe that this convenience difference is more due to modernity, of the threading model being, what 40, 50, 60 years old, and given a clean-ish slate to build a new model, modern language designers did better.

Then why don't see any better designs amongst modern languages?

New languages have an opportunity to add newer, better threading primitives. Yet, its almost always the same stuff: Atomics, mutexes and semaphores. Even Rust uses the same primitives, just with a borrow checker this time. Arguably message passing (erlang, go) is better. But Go still has shared mutable memory and mutexes in its sync library.

> But it raises the idea: if we rethought OS-level preemptible concurrency today (don't call it threads!), could we modernize it and do better even than async?

I'd love to see some thought put into this. Threading doesn't seem like a winner to me.


Ok, you've been programming for years. But didn't learn a lot about threads, apparently.

> Multithreaded code is often much harder to reason about than async code, because threads can interleave executions and threads can be preempted anywhere.

No, green threads / fibres or whatever you want to call them explicitly don't interleave executions. They are a form of cooperative multitasking. Async/await is another form of co-operative multitasking. One former just builds on what we already have. The latter re-invents the universe.

By the by, the blocker for Javascript green threads wasn't preemption, mostly because there isn't any. It's that Javascript has a "run to completion" model. If the DOM calls a javascript event (which is effectively how all javascript is invoked in a browser), it doesn't block, so it always runs to completion. Green threads break that model. It's not a insurmountable break - the DOM events could always still return immediately, but they could start a green thread that returns to them as soon as they block. Thinking about it, the change is possibly smaller than language changes required by async/await.

If you can reason about where an await is, you can reason about where a green thread yields. The only difference is that one of them clutters your syntax and the other doesn't.


> No, green threads / fibres or whatever you want to call them explicitly don't interleave executions.

If you use them with a multithreaded executor (eg in Go), of course they interleave executions. I suppose all your green threads / fibers could run on a single CPU core. But what's the point? How would that be an improvement over what we have now?

I suppose you could make something similar to async/await but with a yield() operation whenever a call wants to block. This would allow blocking read() and so on. But its basically async/await but without declaring functions as async. And without needing to explicitly await. Await points would be implicit and invisible. But if you do that, any function call you make could yield before returning. As a result, you could no longer easily reason about interleaving. I call foo(). Does it yield to other threads before returning? I have no idea. I could read the code of foo(), but maybe foo will change between minor versions of the library.

This would lead to an avalanche of bugs. Lots of javascript code quietly depends on the lack of interleaving for correctness. Javascript guarantees that while my (non async) function runs, no other code gets executed. Adding threads, even if its via cooperative multitasking, would break that invariant. It would break all sorts of programs which are working correctly today.

> The latter re-invents the universe. [...] Thinking about it, the change is possibly smaller than language changes required by async/await.

Did you write much javascript before async/await and before promises? Javascript at the time was already async. We just implemented async execution through callbacks. ("Callback hell"). Over time, functions tended to go down and to the right. Promises were added as 3rd party libraries. Then promises were standardised. And later, async/await was added as syntax to help you work with promises. Async / await in javascript was an incremental change to give us new syntax to do what we were already doing. JS already had an event loop and promises. Async/await just added syntax.

Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

> The only difference is that one of them clutters your syntax and the other doesn't.

One of them is explicit about where and when a thread blocks. Whether or not something is "blocking" (async) is part of the API. Threading (incl cooperative threading) hides this information. Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.


> I suppose all your green threads / fibers could run on a single CPU core.

yes.

> But what's the point? How would that be an improvement over what we have now?

> But its basically async/await but without declaring functions as async.

You answered your own question: yes, you get what you have now, without all the overhead of async, await, promises and futures.

> But if you do that, any function call you make could yield before returning.

A green thread could be an instance of a particular type, so `input = self.yield()` would fail if you aren't a green thread. So no, not "any function" - just ones that instances of a green thread, or are passed a reference to one.

> Does it yield to other threads before returning?

It could if you pass it an instance to a green thread, otherwise it can't.

> This would lead to an avalanche of bugs.

It doesn't. Cooperative multitasking is at least 1/2 a century old at this point. The bugs you're imagining will happen mostly aren't an issue. To the extent they do happen, it's because someone hasn't thought about two control flows modifying the same data structure. Yes, that happens, but it happens in all single threaded code - async included. It's why we hate side effects. It's what Rust famously prevents with its borrow checker even in the face of side effects. It's not avoided by async. The explicit colouring does not help to prevent it - it's just overhead.

FWIW the one issue cooperative multitasking does often introduce is that they can take a long time to execute, so other cooperative tasks don't run in a timely fashion. Exactly the same thing can happen with async of course. It's not usually a problem in browsers, but in embedded solutions where cooperative multitasking is commonly used, it's a real issue because they are often real time. Ask me how I know.

> Javascript guarantees that while my (non async) function runs, no other code gets executed.

This remains true. You are getting confused by your mental model of threads as a form of concurrency. There is no concurrency going on there. Semantically it is near identical to async / await. The principle difference is in async / await, the program is explicitly creating each stack frame on the heap using manually allocated objects. In addition to the mental overhead that creates, it slower than using a real stack like green threads do. But now for the truly bizarre twist. Can you guess how modern javascript engines get around that speed issue? Wait for it .... they create an explicit stack ... that looks like what green threads would use anyway! And as a wonderful side effect - you get real stack back traces again. The irony is almost palpable. https://v8.dev/blog/fast-async

> Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

I agree the ship has sailed at this point. The rest of the assertions you make there are wrong.

This assertion stands out: frozen websites. Can you tell me how they are going to block? There are no blocking calls in javascript now. The things you would await on now would be passed a green thread handle. But the javascript scripts events called from the DOM have no green-thread handle, so they can't block.

> Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.

You don't. You've just been conditioned to think that because you've never done it any other way. But the reality is people have been using cooperative multitasking for a long, long time. It pre-dates threads and async. The issues and bugs you are proclaiming would happen don't arise.


If we invented a new language, sure. Cooperative multitasking might be a fun approach. The avalanche of bugs I’m imagining would come from existing JavaScript code being run in a different context than that in which it was written and tested. If you pass me a callback right now, and I call a(); callback(); b();. I can guarantee that the program doesn’t yield to the event loop or other executions between a() and b(). As I understand it, this guarantee no longer holds with coop. multitasking because your callback can yield to another thread.

Good on the V8 team. Sounds like they’ve figured out a way to get the performance of green threads with the better ergonomics of effects systems (async await). Great!

You sound like an expert in cooperative multithreading. If async await can use real stacks, what actual benefits are there to cooperative multithreading? Why prefer them over what JS has now? Pitch them to me.


> The avalanche of bugs I’m imagining would come from existing JavaScript code being run in a different context than that in which it was written and tested.

Oh, right. As you said, the ship has sailed. I think you could bolt green threads onto javascript now without ill effects - apart from bloating the language. I can't see anything that could go badly (certainly no avalanche of bugs). But in javascript green threads are only mildly more ergonomic than async. I wouldn't be bloating the language for such a small return.

Rust is a different position. The current async implementation has two big black hairs. Firstly, they had to come up with a type-safe way saving the functions current state. By state, I mean what a function normally stores on its stack. What they came up with is a work of art in some ways, but it doesn't work well with the borrow checker. The borrow checker insists you prove that you have exclusive use of a variable while it exists. Things on the stack have a limited lifetime (the function call), so the compiler knows they don't exist for very long. Even with that small lifetime it's a battle, but it's workable. Async persists that state, usually to the heap, which can effectively live forever. That wreaks havoc with the borrow checker, causing comments like this: https://news.ycombinator.com/item?id=37436274, quote: "Yes, async is effectively a much harder version of Rust ...".

The second issue is colouring. In the current Rust async implementation of large chunks of it is left to libraries, like tokio. Each of these libraries has to provide their own I/O. They aren't compatible. So if you want to use a cute new HTTP server, you are out of luck unless they provided a version that talks to the async library you are using.

The library writers do their best to accommodate by providing interfaces to the popular async libraries. That forces them to do a extra work. Whereas before they could just call `std::file::File::read()`, now they have to abstract all the I/O they do to a different module, and provide an implementation of that module for each async library they want to support.

The outcome can only be described as a mess, and that's putting it politely. It's harming uptake of the language. It wasn't like they didn't know it was coming either - there were comments pleading for a better implementation. And it wasn't as if weren't better solutions weren't already apparent - they had green threads before, they made some wrong turns with its implementation that needed to be fixed. And it's not like these solutions were harder to do than the async implementation they came up with. Async needed new standard library features to stabilise (like `Pin<>`) and introduced new keywords - none of which was needed for green threads. (Although some would be useful for an efficient green thread implementation - like knowing the maximum amount of stack a function could use.)

In the face of all that, they persisted with async. You'd need a sociologist to explain how that happened - to my engineering brain it's inexplicable. Unlike Javascript it isn't just mildly ergonomic implementation of the same thing, it's a serious mistake - well worth the effort of throwing out and replacing.


On all that, we have near total agreement. I've been complaining about how broken and half-baked rust's async story is for years - for more or less the same reasons you list above:

- You can't name the type of a impl Future.

- They play terribly with the borrow checker because the borrow checker can't handle self referential types.

- There's no future executor in the standard library. You need 3rd party libraries. And the most common library is tokio, which is a whale.

- Despite all the work, there's still no async streams in the language.

- Pin. !Unpin. pin_project. Unsafe pin_project. What are we even doing.

But async works really well in javascript. Maybe where we disagree is that I don't think any of these issues are because async itself is a bad idea. But, async has become the place dreams go to die in rust. Look at the issues above. They're all problems with rust's type system, borrow checker and standard library.

What I think rust needs is:

- A way to have self-borrows in a struct. Types with self borrows would be implicitly pinned.

- A way to name the return value of a function. Eg let x: ReturnType<some_func>. People have been saying this is right around the corner since 2019.

- Generators. Futures are built on top of generators inside the compiler. But generators have - for some reason - never been exposed in stable rust. I think generators should have been stabilised first - since all the problems you need to solve to make generators work well (self referential types, return values you can name, etc) are things futures need too.

Unfortunately I think that ship has sailed too. I try to avoid async rust whenever I can. Its such a pity. I'm hoping someone makes a rust 2.0 language at some point which fixes this situation.


> I think generators should have been stabilised first - since all the problems you need to solve to make generators work well (self referential types, return values you can name, etc) are things futures need too.

Generators are an interesting case. For example, if you implemented a Vec iterator as a generator, it becomes:

    fn vec_iter(&self) {
       for index in 0..self.len() {
           yield &self[i];
       }
    }
Which is arguably easier to understand than the current event driven formulation, which required you to declare a new type to hold your state, and the code looks like:

    fn next(&self) {
       if (self.index >= self.vec.len()) {
            None
       } else {
            self.index += 1;
            &self.vec[self.index - 1]
       }
    }
Effectively the stack frame has become your type, and sequential code is always so much more compact and clearer than the event driven model. The generator could be implemented as a green thread, but you would never entertain the overhead of creating the new stack needed by the green thread implementation.

However ... the async implemented all the mechanics needed to get rid of that green thread stack allocation when the size of the stack is known, as it is in this case. The state saving stuff they created for async could be used to translate that stack to a type. It would, surprise, surprise, contain just `index` - analogous the iterator type we have to manually create for event drive code. So compiler could translate the green thread to the same implementation as the event driven code, but you get to use the compact (and very familiar) syntax of a stack machine.

I found it interesting to see what happens for a more complex generator - like something that returns every node in a tree. You can do it recursively, which is simple clear code, but you don't know the size of the stack so the trick used for the vec iterator (translating it to a type) can't be used. Or you can manually store the state you stored in the stack with a recursive implementation in a Vec<> instead. Both require a memory allocation, but they are different. One is just normal malloc that must be reallocated and moved as the allocation grows. The other can use the OS's stack implementation, that doesn't move as it grows. If you re-used stacks, the OS's stack implementation would be faster in a long running program.

Notice that the transformation from a generator to async implementation is arguably more complex than the same transformation for green threads, especially for the tree traversal.

That observation is one of the reasons I'm such a strong proponent of green threads. The other is a simpler mental model. Unlike async, you don't have to expose the inner mechanisms it depends on, like futures.


> However ... the async implemented all the mechanics needed ...

As I understand it, the implementation of async in the rust compiler grew out of the implementation for generators in nightly. Its the same continuation-passing transformation that lets you implement both await and yield in your fictional example.

> Notice that the transformation from a generator to async implementation is arguably more complex than the same transformation for green threads, especially for the tree traversal.

Yeah for sure. Another nice thing about green threads is that the compiler doesn't need to invert the call stack. I suspect you'd get smaller binaries in many cases. A lot of the complexity of async in rust comes from moving stack variables into a hidden struct as part of this transformation. For example, this function:

    async fn foo() -> impl Future {
        let x = 5;
        let y = &x;
        await someexpr();
        // ...
    }
Emits something like this:

    enum FooFuture {
        AwaitPoint1 { x: usize, y: &'a usize }
    }
But y is a reference to x - which makes this struct impossible to actually write using the rust programming language. Hence pin and all that. This is a very common pattern, but the rust lifetime syntax makes this struct impossible to express.

> That observation is one of the reasons I'm such a strong proponent of green threads. The other is a simpler mental model. Unlike async, you don't have to expose the inner mechanisms it depends on, like futures.

Fair. But as I said earlier in this thread, I like the mechanism (futures) are exposed. I like that "async" is part of a function signature. I like that you need to be explicit about which functions yield, when, and where. I want programming languages to have more effect systems - for example, it would be great to have a nopanic effect. I just ... find it much easier to enjoy async in javascript.


Now you are comparing single threaded code with multi threaded, which is a completely different axis to async vs sync. Just take a look at C#'s async, where you have both async and multi threading, with all the possible combinations of concurrency bugs you can imagine.


Of course I'm comparing them. Threading and async are two solutions to the same problem: How do you write high performance event driven systems like network services? How do you solve the C10K problem (or more recently the C10M problem)?

If you use a thread per connection (or green threads like Go), you don't also need async. If you have async (eg nodejs), you can get great performance without threads. You're right that they can also be combined - either within a single process (like tokio in rust). Or via multi-process configurations (eg one nodejs instance per core, all behind nginx). But they don't need to be. Go (green threads) and Nodejs (async, single threads) both work well.

Of course we're comparing them. We all want to know who wore it better


You wrote:

> The job wouldn’t have been done. They would have needed threads. And mutexes. And spin locks. And atomics. And semaphores. And message queues. And - in my opinion - the result would have been a much worse language.

My point is that you do need mutexes, spin locks, etc with async as well, given that you have a multi threaded platform. So no, we have basically 2x2 stuff we are talking about with very different properties (async/sync x single/multi threaded).


> My point is that you do need mutexes, spin locks, etc with async as well, given that you have a multi threaded platform.

No, Javascript isn't a shared-memory multi threaded platform. It would only need mutexes and so on if we added threads to javascript, as the comment I was replying to suggested should have happened:

> At any step in that sequence, the language could have introduced green threads and the job would have been done


That's why I brought up C# as an example of a language with multi threaded async. But Kotlin is another example.


> Rust has green threads in the early days and abandoned them in favour of async / await. Granted the original green thread implementation needed a bit of refinement - making every low level choose between event driven and blocking on every invocation was a mistake.

That's a mischaraterization. They were abandoned because having green threads introduces non-trivial runtime. It means Rust can't run on egzotic architectures.

> It sounds like Zig with its pluggable I/O interface finally got it right

That remains to be seen. It looks good, with emphasis on looks. Who knows what interesting design constraints and limitation that entails.

Looking at comptime, which is touted as Zig's mega feature, it does come at expense of a more strictly typed system.


What if process_the_character takes multiple seconds waiting on a network request?


The key to understanding is green threads are semantically identical to async - just formulated a little differently. The code I gave left out a lot of details. This:

    while (char = read())
        process_the_character(char);
It may look more like this:

    element.onkeydown = window.greenThread(processKeyEvents, ...);

    ...

    function processKeyEvents(green_thread, ...) {
        while (event = green_thread.yield(...)) {
            /* process the key event */
            var foo = call_some_function_that_blocks(...);
            /* more processing */
        }
     }
  
You can only call a function that blocks (like `green_thread.yield()`) from within a green thread. Attempt it outside of a green thread and they throw an exception.

Code is effectively still coloured, because a call that can block can't be called from a context that doesn't allow block (like DOM events). async makes this colouring obvious but it comes at the expense of new keywords, new concepts like promises and futures, and manually creating the stack using linked object on the heap. A real stack is far faster than allocating stuff on the heap. But to repeat the key observation above: async and green threads are semantically near identical, it is only the implementation is different.

One selling point used for async is it makes blocking calls obvious, and that would somehow avoid bugs. The claim (willfully?) ignored that green threads (aka, cooperative multiasking) were the earliest form of multitasking - they have been around for over 50 years at this point. They were never a source of the sort of bugs they claimed would happen.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: