Live data from Hacker News

What async promised and what it delivered

causality.blog

311–317 of 317 posts

Re: What async promised and what it delivered

#311

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.…

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.

Re: What async promised and what it delivered

#312

Earlier quoted context omitted.

> 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 threa…

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. An…

> 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.

Re: What async promised and what it delivered

#313

Earlier quoted context omitted.

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. An…

> 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…

> 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.

Re: What async promised and what it delivered

#314
post #87

> Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose different paths. Not really. The author provides Go as evidence, but Go's CSP-based approach far predates the popularity of async/await. Meanwhile, Zig's approach still has function coloring, it's just that one color is "I/O function" and the other is "non-I/O f…

Author here. You're right in the sense that some forms of function coloring have good tradeoffs, though I think that's a watered down version of that original meaning. The point is that async/await tracks the wrong thing in the wrong direction, not that "tracking effects in types is bad." In a proper effect system, subsumption goes the right way: pure is universally callable and handled effects disappear. Async/await inverts this: the "simple" color (sync) is the restricted one.

Re: What async promised and what it delivered

#315

> OS threads are expensive: an operating system thread typically reserves a megabyte of stack space Why is reserving a megabyte of stack space "expensive"? > and takes roughly a millisecond to create I'm not sure where this number is from, it seems off by a few orders of magnitude. On Linux, thread creation is closer to 10 microseconds.

Author here. I definite brought some old bias and history to this. At the time address space was more limited (32-bit) and kernel interfaces were O(n), but you're right that these issues are not as relevant today and the numbers don't add up. I'll make a correction.

Re: What async promised and what it delivered

#316
post #125
post #107

Earlier quoted context omitted.

What should people read to learn about structured concurrency?

I think the clearest sales pitch comes from this post from the author of Trio, which is an implementation of structured concurrency for Python: https://vorpus.org/blog/notes-on-structured-concurrency-or-g... .

Thank you. So it's more or less running (certain specified) function calls in parallel? Sounds nice, but what happens to upward funargs? I assume first-class continuations are right out ...
Post reply on HN