6 ms·
Async/Await is finally back in Zig
- barddoo 11mo agoWhat changed, why it matters, and how to use the new API.
- ajross 11mo agoIs 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."
- amelius 11mo agoWhat is wrong about C++ exceptions?
- ajross 11mo ago"Nothing", in principle. But they're bug factories in practice. It's really easy to throw "past" cleanup code in a language where manual resource management remains the norm. It's not that they can't be used productively. It's that they probably do more harm than good on balance. And I think async mania is getting there. It was a revelation when node showed it to us 15 years ago. But it's evolved in bad directions, IMHO.
- baq 11mo agoYeah node showed that a native async single threaded runtime can be performant. Seems like this knowledge was lost to the world somewhere along windows vista; everyone who has had to ever develop in the cooperative world of early winapi could tell you that easily.
- efdee 11mo agoC# had async/await long before Javascript/node. Not that big a revelation ;-)
- ajross 11mo ago.NET wasn't the first either. Lisps were doing continuations in the 70's. But "invented" and "revealed" are different verbs for a reason. The release of node.js and it's pervasively async architecture changed the way a lot of people thought about how to write code. For the better in a few ways. But the resulting attempt to shoe-horn the paradigm into legacy and emerging environments that demanded it live in a shared ecosystem with traditional "blocking" primitives and imperative paradigms has been a mess.
- mrsmrtss 11mo agoI think you're underestimating the role of .NET in this. It was .NET that popularized this concept for the masses, and from there it spread to other languages including JavaScript, which also borrowed the exact same async/await keywords from C#.
- KerrAvon 11mo agoFor one thing, they’re expensive and viral. “Zero overhead” implementations don’t take into account the need for unwind tables. For every function/method that might be thrown across. They’re disabled in a lot of production environments for this reason.
- amelius 11mo agoBut if you explicitly handle exceptions using IF statements then that's overhead too, right?
- arbitrandomuser 11mo agoyes but i think branch prediction essentialy makes them zero overhead
- neonz80 11mo agoThat's a different type of overhead than having unwind tables. With exceptions you wouldn't need a branch after each function call at all.
- amelius 11mo agoBut a branch that is (almost) never taken has an overhead close to the overhead of a NOP instruction, which may be negligible on modern architectures.
- neonz80 11mo agoThe CPU can not remember an infinite number of branches. Also, many branches will increase code size. With exceptions the unwind tables and unwind code can be placed elsewhere and not take up valuable L1 cache.
- amelius 11mo ago> The CPU can not remember an infinite number of branches. I suspect a modern CPU has a branch instruction saying "This branch will never be taken except in exceptions, so assume this branch is not taken". But I must admit I haven't seriously looked at assembly language for some time. (EDIT: yes, modern CPUs including x86 and ARM allow the programmer/compiler to hint if a branch is expected to be taken). > Also, many branches will increase code size. I'd like to see some data on that. Of course branches take code size, but how much is that percentage-wise? I suspect not much.
- jandrewrogers 11mo agoThere are cases in systems-y code where it is not safe to unwind the stack in the ordinary way and it is difficult to contain the side-effects. These can be non-obvious and subtle edge cases that are often difficult to see and tricky to handle correctly. C++ today is primarily used in code contexts where these kinds of issues can occur. This is why it is a standard practice to disable exceptions at build time i.e. -fno-exceptions. With the benefit of hindsight, explicit handling and unwinding has proven to be safer and more reliable.
- amelius 11mo agoBut you can implement exceptions by using the same IF statement approach you would use for manual error handling. No need for unwinding tables and such if that optimization is a bridge too far for your specific target platform.
- jayd16 11mo agoI really wish people would get over the coloring meme. Knowing if a function will yield the thread is actually extremely relevant knowledge you want available.
- pton_xd 11mo agoLook at the node.js APIs: readFile, readFileSync, writeFile, writeFileSync ... and on and on. If that's not a meme then I don't know what is.
- rafaelmn 11mo agoAnd the alternative without async-await is ? blocking the event loop or the callback pyramid. Node is one place where async-await has zero counter arguments and every alternative is strictly worse.
- luke5441 11mo agoThey could have added threads to Node as well? Granted, it would have been a lot of difficult work.
- jayd16 11mo agoYou mean like with web workers or something?
- luke5441 11mo agoWith a shared interpreter/process state, like Python, Java, C, C++, ... Node is not a web page, so no reason to limit it to the same patterns. Then, the next issue would be thread safety. But that could be treated as a separate problem.
- dns_snek 11mo agoLosing threads and moving to the async I/O model was the motivation behind Node in the first place. https://nodejs.org/en/about https://nodejs.org/en/about
- the__alchemist 11mo agoConcur. I build my own tools in rust when I have to just to avoid it. It is splitting rust into 2 ecosystems, and I wish it didn't exist because it's a big compatibility barrier. We should be moving towards fewer of these; not more. Make code and applications easier to interop; Async makes it more difficult.
- echelon 11mo agoI can't stand this aversion to async/await. It's not a big deal. I don't understand why async code is being treated as dangerous or as rocket science. You still maintain complete control, and it's straightforward. Now that we know about the "futurelock" issue, it will be addressed. I'm sure Rust and the cargo/crates ecosystem will even grow the ability to mark crates as using async so if you really care to avoid them in your search or blow up at compile time upon import, you can. I've been asking for that feature for unsafe code and transitive dependency depth limits.
- jeltz 11mo agoBecause async Rust is a lot harder to reason about than sync code. And I want my code to be as easy to reason about as possible.
- csande17 11mo agoYour comment is downvoted as I write this, but I kind of think Zig's new design agrees with you. It uses the terms "async" and "await", but the API design looks more similar to traditional threading (like Rust's thread::spawn and join() APIs). With the fun distinction that you can choose whether your program uses actual threads, or coroutines, or just runs everything synchronously without changing any of your code.
- rr808 11mo agoWith Java 25 virtual threads, async definitely is no longer required and I hope it dies a slow and painful death. We have projects at work that have never more than 3 concurrent users that use rxjava and are a nightmare to work on.
- dilawar 11mo agoSomeone 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-async/90160 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.
- lukaslalinsky 11mo agohttps://en.wikipedia.org/wiki/C10k_problem https://en.wikipedia.org/wiki/C10k_problem Because when you require 1 thread per 1 connection, you have trouble getting to thousands of active connections and people want to scale way beyond that. System threads have overhead that makes them impractical for this use case. The alternatives are callbacks, which everybody hates and for a good reason. Then you have callbacks wrapped by Futures/Promises. And then you have some form of coroutines. Keeping in mind that what Zig is introducing is not what languages call async/await. It's more like the I/O abstraction inside Java, where you can use the same APIs with platform threads and virtual threads, but in Zig, you will need to pass the io parameter around, in Java, it's done in the background.
- troupo 11mo ago> The alternatives are callbacks No. The alternative is lightweight/green threads and actors. The thing with await is that it can be retrofitted onto existing languages and runtimes with relatively little effort. That is, it's significantly less effort than retrofitting an actual honest-to-god proper actor system a la Erlang.
- antihero 11mo agoIsn’t await often just sugar around the underlying implementation be that greenthreads, epoll, picoev, etc?
- deleted 11mo ago[deleted]
- devnull3 11mo agoAll the stunts in async/await or goroutines in go stem from the fact that there is no support for something lighter than posix threads in kernel. Shouldn't the OS kernel innovate in this area instead of different languages in userland attempting to solve it?
- barddoo 11mo agoFair. The languages have to come up with something based on APIs that were not meant for that, like io_uring, etc.
- nananana9 11mo agoAsync/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.
- messe 11mo agoZig's implementation is in userspace.
- pyrolistical 11mo ago> 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. Where is concurrent data writing in the example?
- barddoo 11mo agoThe requests were made concurrently. I don't understand your question. Passing in an array or a pointer does not matter.
- kbd 11mo agoI 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.