Is it time now to say that async was a mistake, a-la C++ exceptions? The recent futurelock discussion[1] more or less solidified for me that this is all just a mess. Not just that one bug, but the coloring issue mentioned in the blog post (basically async "infects" project code requiring that you end up porting or duplicating almost everything -- this is especially true in Python). The general cognitive load of debugging inside out code is likewise really high, even if the top-level expression of the loop generator or whatever is clean.
And it's all for, what? A little memory for thread stacks (most of which ends up being a wash because of all the async contexts being tossed around anyway -- those are still stacks and still big!)? Some top-end performance for people chasing C10k numbers in a world that has scaled into datacenters for a decade anyway?
Not worth it. IMHO it's time to put this to bed.
[1] No one in that thread or post has a good summary, but it's "Rust futures consume wakeup events from fair locks that only emit one event, so can deadlock if they aren't currently being selected and will end up waiting for some other event before doing so."
Someone has historical insights into why async/await seems to have taken over the world?
I often write Rust and I don't find it very attractive, but so many good projects seem to advertise it as a "killer feature". Diesel.rs doesn't have async, and they claim that perf improvement may not be worth it (https://users.rust-lang.org/t/why-use-diesel-when-its-not-as...).
For a single threaded JS program, async makes a lot of sense. I can't imagine any alternative pattern to get concurrency so cleanly.
Async/await feels very misguided to me. It's an extremely complex language feature for something that can be done way better, completely in userspace.
You can implement stackful coroutines yourself in C/C++, you need like 30 lines of assembly (as you can't switch stack pointers and save registers onto the stack from most languages). This is WAY better than what you could do for example with the way more convoluted C++ co_async/co_await for two reasons:
1. Your coroutine has an actual stack - you don't have to allocate a new "stack frame" on the heap for every "stack frame", e.g. every time you call a function and await it.
2. You don't need special syntax for awaiting - any function can just call your Yield() function, which just saves the registers onto the stack and jumps out of the coroutine.
Minicoro [1] is a single-file library that implement this in C. I have yet to dig into the Zig implementation - maybe it's better than the C++/Rust ones, but the fact they call it "async/await" doesn't bring me much hope.
> Despite the completion order varying, each task correctly writes to its designated position in the results array, showing proper concurrent data handling.
Huh? It’s not like the entire array was passed into each task. Each task just received a pointer to an usize to write to.
I wrote my shell prompt in Zig years ago in part because I was interested to use its async/await to run all the git calls in parallel for the git status. My prompt is still fast despite never having parallelized things -- slightly slower now after adding Jujutsu status -- but I'm looking forward to getting to do the thing I originally wanted and have my super fast shell prompt.
To speak to the Zig feature: as a junior I kept bugging the seniors about unit testing and how you were supposed to test things that did IO. An explanation of "functional core imperative shell" would have been helpful, but their answer was: "wrap everything in your own classes, pass them everywhere, and provide mocks for testing". This is effectively what Zig is doing at a language level.
It always seemed wrong to me to have to wrap your language's system libraries so that you could use them the "right way" that is testable. It actually turns out that all languages until Zig have simply done it wrong, and IO should be a parameter you pass to any code that needs it to interact with the outside world.
8 comments
[ 1698 ms ] story [ 721 ms ] threadAnd it's all for, what? A little memory for thread stacks (most of which ends up being a wash because of all the async contexts being tossed around anyway -- those are still stacks and still big!)? Some top-end performance for people chasing C10k numbers in a world that has scaled into datacenters for a decade anyway?
Not worth it. IMHO it's time to put this to bed.
[1] No one in that thread or post has a good summary, but it's "Rust futures consume wakeup events from fair locks that only emit one event, so can deadlock if they aren't currently being selected and will end up waiting for some other event before doing so."
I often write Rust and I don't find it very attractive, but so many good projects seem to advertise it as a "killer feature". Diesel.rs doesn't have async, and they claim that perf improvement may not be worth it (https://users.rust-lang.org/t/why-use-diesel-when-its-not-as...).
For a single threaded JS program, async makes a lot of sense. I can't imagine any alternative pattern to get concurrency so cleanly.
Shouldn't the OS kernel innovate in this area instead of different languages in userland attempting to solve it?
You can implement stackful coroutines yourself in C/C++, you need like 30 lines of assembly (as you can't switch stack pointers and save registers onto the stack from most languages). This is WAY better than what you could do for example with the way more convoluted C++ co_async/co_await for two reasons:
1. Your coroutine has an actual stack - you don't have to allocate a new "stack frame" on the heap for every "stack frame", e.g. every time you call a function and await it.
2. You don't need special syntax for awaiting - any function can just call your Yield() function, which just saves the registers onto the stack and jumps out of the coroutine.
Minicoro [1] is a single-file library that implement this in C. I have yet to dig into the Zig implementation - maybe it's better than the C++/Rust ones, but the fact they call it "async/await" doesn't bring me much hope.
Huh? It’s not like the entire array was passed into each task. Each task just received a pointer to an usize to write to.
Where is concurrent data writing in the example?
To speak to the Zig feature: as a junior I kept bugging the seniors about unit testing and how you were supposed to test things that did IO. An explanation of "functional core imperative shell" would have been helpful, but their answer was: "wrap everything in your own classes, pass them everywhere, and provide mocks for testing". This is effectively what Zig is doing at a language level.
It always seemed wrong to me to have to wrap your language's system libraries so that you could use them the "right way" that is testable. It actually turns out that all languages until Zig have simply done it wrong, and IO should be a parameter you pass to any code that needs it to interact with the outside world.