9 ms·
Zig's New Async I/O
https://www.youtube.com/watch?v=mdOxIc0HM04 https://www.youtube.com/watch?v=mdOxIc0HM04
- sanity 1y agoAndrew Kelley is one of my all-time favorite technical speakers, and zig is packed full of great ideas. He also seems to be a great model of an open source project leader.
- throwawaymaths 1y agoI would say zig is not just packed full of great ideas, there's a whole graveyard of ideas that were thrown out (and a universe of ideas that were rejected out of hand). Zig is full of great ideas that compose well together.
- bcardarella 1y agoOne that was tossed in 0.15 was `usingnamespace` which was a bit rough to refactor away from.
- ancarda 1y agoI really hope this is going to be entirely optional, but I know realistically it just won't be. If Rust is any example, a language that has optional async support, async will permeate into the whole ecosystem. That's to be expected with colored functions. The stdlib isn't too bad but last time I checked a lot of crates.io is filled with async functions for stuff that doesn't actually block. Async clearly works for many people, I do fully understand people who can't get their heads around threads and prefer async. It's wonderful that there's a pattern people can use to be productive! For whatever reason, async just doesn't work for me. I don't feel comfortable using it and at this point I've been trying on and off for probably 10+ years now. Maybe it's never going to happen. I'm much more comfortable with threads, mutex locks, channels, Erlang style concurrency, nurseries -- literally ANYTHING but async. All of those are very understandable to me and I've built production systems with all of those. I hope when Zig reaches 1.0 I'll be able to use it. I started learning it earlier this month and it's been really enjoyable to use.
- eddd-ddde 1y ago> I do fully understand people who can't get their heads around threads and prefer async. Those are independent of each other. You can have async with and without threads. You can have threads with and without async.
- rowanG077 1y agoYeah, it always mystifies me when people talking about async vs threads when they are completely orthogonal concepts. It doesn't give me the feeling they understand what they are talking about.
- int_19h 1y agoIt's because JavaScript was the thing that popularized async/await for many developers, and they aren't aware that it originated in a language (C#) and runtime (.NET) that are fully multithreaded.
- speed_spread 1y agoI agree, async is way more popular than it should be. At least (AFAIU) Zig doesn't color functions so there won't be a big ecosystem rift between blocking and async libraries.
- flohofwoe 1y ago> I'm much more comfortable with threads The example code shown in the first few minutes of the video is actually using regular OS threads for running the async code ;) The whole thing is quite similar to the Zig allocator philosophy. Just like an application already picks a root allocator to pass down into libraries, it now also picks an IO implementation and passes it down. A library in turn doesn't care about how async is implemented by the IO system, it just calls into the IO implementation it got handed from the application.
- throwawaymaths 1y agoor you can pick one manually. Or, you can pick more than one and use as needed in different parts of your application. (probably less of a thing for IO than allocator).
- gethly 1y agoI almost fell out of my chair at 22:06 from laughter :D
- synergy20 1y agohave not played with zig for a while, remain in the C world. with the cleanup attribute(a cheap "defer" for C), and the sanitizers, static analysis tools, memory tagging extension(MTE) for memory safety at hardware level, etc, and a zig 1.0 still probably years away, what's the strong selling point that I need spend time with zig these days? Asking because I'm unsure if I should re-try it.
- flohofwoe 1y agoMy 2ct: A much more useful stdlib (that's also currently the most unstable part though, and I don't agree with all the design decisions in the stdlib - but mostly still better than a nearly useless stdlib like C provides - and don't even get me started about the C++ stdlib heh), integrated build system and package manager (even great for pure C/C++ projects), and comptime with all the things it enables (like straightforward reflection and generics). It also fixes a shitton of tiny design warts that we've become accustomed to in C (also very slowly happening in the C standard, but that will take decades while Zig has those fixes now). Also, probably the best integration with C code you can get outside of C++. E.g. hybrid C/Zig projects is a regular use case and has close to no friction. C won't go away for me, but tinkering with Zig is just more fun :)
- cassepipe 1y agoCould you expand on what are the design decisions you disapprove of ? You got me curious
- flohofwoe 1y agoMany parts of the stdlib don't separate between public interface and internal implementation details. It's possible to accidentially mess with data items which are clearly meant to be implementation-private. I think this is because many parts of the stdlib are too object-oriented, for instance the containers are more or less C++ style objects, but this style of programming really needs RAII and restricted visibility rules like public/private (now Zig shouldn't get those, but IMHO the stdlib shouldn't pretend that those language features exist). As a sister comment says, Zig is a great programming language, but the stdlib needs some sort of basic and consistent design philosophy whitch matches the language capabilities. Tbf though, C gets around this problem by simply not providing a useful stdlib and delegating all the tricky design questions to library authors ;)
- AndyKelley 1y agotext version: https://andrewkelley.me/post/zig-new-async-io-text-version.html https://andrewkelley.me/post/zig-new-async-io-text-version.h...
- dang 1y agoThanks - we'll switch the URL to that above and re-up the thread.
- geon 1y ago> Now that it's only using one thread, it deadlocks, because the consumer is waiting to get something from the queue, and the producer is scheduled to run, but it has not run yet. Is the reason that the queue has a zero buffer size (always "full"), so that the consumer must consume the `flavor_text` before the producer can return? Queue source: > /// When buffer is full, producers suspend and are resumed by consumers. https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1465C1-L1465C73 https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1...
- pragmatic 1y agohttps://andrewkelley.me/post/zig-new-async-io-text-version.html https://andrewkelley.me/post/zig-new-async-io-text-version.h... Text version. The desynced video makes the video a bit painful to watch.
- dang 1y agoYup, we'll use the text version and keep the video link in the toptext.
- PaulHoule 1y agoIt seems to me that async io struggles whenever people try it. For instance it is where Rust goes to die because it subverts the stack-based paradigm behind ownership. I used to find it was fun to write little applications like web servers in aio Python, particularly if message queues and websockets were involved, but for ordinary work you're better off using gunicorn. The trouble is that conventional async i/o solutions are all single threaded and in an age where it's common to have a 16 core machine on your desktop it makes no sense. It would be like starting a chess game dumping out all your pieces except for your King. Unfashionable languages like Java and .NET that have quality multithreaded runtimes are the way to go because they provide a single paradigm to manage both concurrency and parallelism.
- erichocean 1y agoProject Loom makes Java in particular really nice, virtual threads can "block" without blocking the underlying OS thread. No callbacks at all, and you can even use Structured Concurrency to implement all sorts of Go- and Erlang-like patterns. (I use it from Clojure, where it pairs great with the "thread" version of core.async (i.e. Go-style) channels.)
- vlovich123 1y ago> Unfashionable languages like Java and .NET that have quality multithreaded runtimes are the way to go because they provide a single paradigm to manage both concurrency and parallelism. At the cost of not being able to actually provide the same throughput, latency, or memory usage that lower level languages that don't enforce the same performance pessimizing abstractions on everything can. Engineering is about tradeoffs but pretending like Java or .NET have solved this is naiive.
- pron 1y ago> At the cost of not being able to actually provide the same throughput, latency, or memory usage Only memory usage is true with regards to Java in this context (.NET actually doesn't offer a shared thread abstraction; it's Java and Go that do), and even that is often misunderstood. Low-level languages are optimised for minimal memory usage, which is very important on RAM-constrained devices, but is could be wasting CPU on most machines: https://youtu.be/mLNFVNXbw7I https://youtu.be/mLNFVNXbw7I This optimisation for memory footprint also makes it harder for low-level languages to implement user-mode threading as efficiently as high-level languages. Another matter is that there are two different use cases for asynchronous constructs that may tempt implementors to address them with a single implementation. One is the generator use case. What makes it special is that there are exactly two communicating parties, and both of their state may fit in the CPU cache. The other use case is general concurrency, primarily for IO. In that situation, a scheduler juggles a large number of user-mode threads, and because of that, there is likely a cache miss on every context switch, no matter how efficient it is. However, in the second case, almost all of the performance is due to Little's law rather than context switch time (see my explanation here: https://inside.java/2020/08/07/loom-performance/ https://inside.java/2020/08/07/loom-performance/). That means that a "stackful" implementation of user-mode threads can have no significant performance penalty for the second use case (which, BTW, I think has much more value than the first), even though a more performant implementation is possible for the first use case. In Java we decided to tackle the second use case with virtual threads, and so far we've not offered something for the first (for which the demand is significantly lower). What happens in languages that choose to tackle both use cases with the same construct is that in the second and more important use case they gain no more than negligible performance (at best), but they're paying for that with a substantial degradation in user experience.
- MRson 1y agoI don't understand, why is allocation not part of IO? This seems like effect oriented programming with a kinda strange grouping: allocation, and the rest (io).
- andyferris 1y agoMost effect-tracking languages have a GC and allocations/deallocations are handled implicitly. To perform what we normally call "pure" code requires an allocator but not the `io` object. Code which accepts neither is also allocation-free - something which is a bit of a challenge to enforce in many languages, but just falls out here (from the lack of closures or globally scoped allocating/effectful functions - though I'm not sure whether `io` or an allocator is now required to call arbitrary extern (i.e. C) functions or not, which you'd need for a 100% "sandbox").
- thefaux 1y agoI find the direction of zig confusing. Is it supposed to be a simple language or a complex one? Low level or high level? This feature is to me a strange mix of high and low level functionality and quite complex. The io interface looks like OO but violates the Liskov substitution principle. For me, this does not solve the function color problem, but instead hides it. Every function with an IO interface cannot be reasoned about locally because of unexpected interactions with the io parameter input. This is particularly nasty when IO objects are shared across library boundaries. I now need to understand how the library internally manages io if I share that object with my internal code. Code that worked in one context may surprisingly not work in another context. As a library author, how do I handle an io object that doesn't behave as I expect? Trying to solve this problem at the language level fundamentally feels like a mistake to me because you can't anticipate in advance all of the potential use cases for something as broad as io. That's not to say that this direction shouldn't be explored, but if it were my project, I would separate this into another package that I would not call standard.
- mordnis 1y agoCouldn't the same thing be said about functions that accept allocators?
- metaltyphoon 1y agoI don't think so because the result of calling an allocator is either you got memory or you don’t, while the IO here will be “it depends”
- chotmat 1y agoI don't get it, what's the difference between "got or don't" vs "it depends"?
- metaltyphoon 1y agoThe allocator’s output is only two: either you get memory or you don’t. To quote GP > Every function with an IO interface cannot be reasoned about locally because of unexpected interactions with the io parameter input This means with IO interface is not quiet clear what WILL happen so it “depends”
- nrds 1y agoTL;DR. Zig now has an effect system with multiple handlers for alloc and io effects. But the designer doesn't know anything about effect systems and it's not algebraic, probably. Will be interesting to see how this develops.
- comex 1y agoIt's worth noting that this is not async/await in the sense of essentially every other language that uses those terms. In other languages, when the compiler sees an async function, it compiles it into a state machine or 'coroutine', where the function can suspend itself at designated points marked with `await`, and be resumed later. In Zig, the compiler used to support coroutines but this was removed. In the new design, `async` and `await` are just functions. In the threaded implementation used in the demo, `await` just blocks the thread until the operation is done. To be fair, the bottom of the post explains that there are two other Io implementations being planned. One of them is "stackless coroutines", which would be similar to traditional async/await. However, from the discussion so far this seems a bit like vaporware. As discussed in [1], andrewrk explicitly rejected the idea of just (re-)adding normal async/await keywords, and instead wants a different design, as tracked in issue 23446. But in issue 23446 the seems to be zero agreement on how the feature would work, how it would improve on traditional async/await, or how it would avoid function coloring. The other implementation being planned is "stackful coroutines". From what I can tell, this has more of a plan and is more promising, but there are significant unknowns. The basis of the design is similar to green threads or fibers. Low-level code generation would be identical to normal synchronous code, with no state machine transform. Instead, a library would implement suspension by swapping out the native register state and stack, just like the OS kernel does when switching between OS threads. By itself, this has been implemented many times before, in libraries for C and in the runtimes of languages like Go. But it has the key limitation that you don't know how much stack to allocate. If you allocate too much stack in advance, you end up being not much cheaper than OS threads; but if you allocate too little stack, you can easily hit stack overflow. Go addresses this by allocating chunks of stack on demand, but that still imposes a cost and a dependency on dynamic allocation. andrewrk proposes [2] to instead have the compiler calculate the maximum amount of native stack needed by a function and all its callees. In this case, the stack could be sized exactly to fit. In some sense this is similar to async in Rust, where the compiler calculates the size of async function objects based on the amount of state the function and its callees need to store during suspension. But the Zig approach would apply to all function calls rather than treating async as a separate case. As a result, the benefits would extend beyond memory usage in async code. The compiler would statically guarantee the absence of stack overflow, which benefits reliability in all code that uses the feature. This would be particularly useful in embedded where, typically, reliability demands are high and memory available is low. Right now in embedded, people sometimes use a GCC feature ("-fstack-usage") that does a similar calculation, but it's messy enough that people often don't bother. So it would be cool to have this as a first-class feature in Zig. But. There's a reason that stack usage calculators are uncommon. If you want to statically bound stack usage: First, you have to ban recursion, or else add some kind of language mechanism for tracking how many times a function can possibly recurse. Banning recursion is common in embedded code but would be rather annoying for most codebases. Tracking recursion is definitely possible, as shown by proof languages like Agda or Coq that make you prove termination of recursive functions - but those languages have a lot of tools that 'normal' languages don't, so it's unclear how ergonomic such a feature could be in Zig. The issue [2] doesn't have much concrete discussion on how it would work. Second, you have to ban dynamic calls (i.e. calls to function pointers), because if you don't know what function you're calling, you don't know how much stack it will use. This has been the subject of more concrete design in [3] which proposes a "restricted" function pointer type that can only refer to a statically known set of functions. However, it remains to be seen how ergonomic and composable this will be. Zooming back out: Personally, I'm glad that Zig is willing to experiment with these things rather than just copying the same async/await feature as every other language. There is real untapped potential out there. On the other hand, it seems a little early to claim victory, when all that works today is a thread-based I/O library that happens to have "async" and "await" in its function names. Heck, it seems early to finalize an I/O library design if you don't even know how the fancy high-performance implementations will work. Though to be fair, many applications will get away just fine with threaded I/O, and it's nice to see a modern I/O library design that embraces that as a serious option. [1] https://github.com/ziglang/zig/issues/6025#issuecomment-3072857526 https://github.com/ziglang/zig/issues/6025#issuecomment-3072... [2] https://github.com/ziglang/zig/issues/157 https://github.com/ziglang/zig/issues/157 [3] https://github.com/ziglang/zig/issues/23367 https://github.com/ziglang/zig/issues/23367
- pyrolistical 1y agoOh boy. Example 7 is a bit of a mindfuck. You get the returned string in both in await and cancel. Feels like this violates zig “no hidden control flow” principle. I kinda see how it doesn’t. But it sure feels like a violation. But I also don’t see how they can retain the spirit of the principle with async code.
- rdtsc 1y ago> Feels like this violates zig “no hidden control flow” principle. A hot take here is that the whole async thing is a hidden control flow. Some people noticed that ever since plain callbacks were touted as a "webscale" way to do concurrency. The sequence of callbacks being executed or canceled forms a hidden, implicit control flow running concurrently with the main control logic. It can be harder to debug and manage than threads. But that said, unless, Zig adds a runtime with its own scheduler and turns into a bytecode VM, there is not much it can do. Co-routines and green threads have been done before in C and C-like languages, but not sure how easily the would fit with Zig and its philosophy.
- throwawaymaths 1y agohidden control flow means no control flow occurs outside of function boundaries, keywords, short circuiting operators, or builtins. i believe there is a plan for a asyncresume and asyncsuspend builtins that show the actual sites where control flow happens.
- int_19h 1y agoThey have plans for multiple implementations including green threads and stackless.
- rdtsc 1y agoThat's pretty neat. Thanks for pointing it out. The abstraction on top still async based and I agree it makes sense for Zig. But in general I don't like that abstraction. I like it when it's flipped around -- the abstraction is process/thread/green thread-like and sending messages or events around. Underneath it may involved having a few IO pollers with select/epoll/io_uring, a thread pool, etc. But the top level API doesn't handle promises, futures, callback, deferreds, etc. I am thinking of Go's goroutines, BEAM VM processes, or even plain threads or processes talking over the network or locally via queue.
- deleted 1y ago[deleted]
- andyferris 1y agoPersonally I find this really cool. One thing I like about the design is it locks in some of the "platforms" concepts seen in other languages (e.g. Roc), but in a way that goes with Zig's "no hidden control flow" mantra. The downstream effect is that it will be normal to create your own non-posix analogue of `io` for wherever you want code to hook into. Writing a game engine? Let users interact with a set of effectful functions you inject into their scripts. As a "platform" writer (like the game engine), essentially you get to create a sandbox. The missing piece may be controlling access to calling arbitrary extern C functions - possibly that capability would need to be provided by `io` to create a fool-proof guarantees about what some code you call does. (The debug printing is another uncontrolled effect).
- ozgrakkurt 1y agoReally really hope they make it easy and ergonomic to integrate with single threaded cooperative scheduling paradigm like seastar or glommio. I wrote a library that I use for this but it would be really nice to be able to cleanly integrate it into async/await. https://github.com/steelcake/csio https://github.com/steelcake/csio
- lukaslalinsky 1y agoI wrote a library that does single threaded cooperative scheduling and async I/O and from the ground up, it was designed to implement this interface. https://github.com/lalinsky/zio https://github.com/lalinsky/zio
- SkiFire13 1y agoThat's quite an unfortunate name given there's already a famous async framework named ZIO for Scala https://github.com/zio/zio https://github.com/zio/zio
- lukaslalinsky 1y agoI knew about this, but I think the language scope is so different that it's OK.
- SV_BubbleTime 1y agoIf I know anything about anything it is that a new language debuting a new async/await plan will be well received by casual and expert alike.
- nialv7 1y agoThis just sounds like a threadpool implementation to me. Sure you can brand it async, but it's not what we generally mean when we say async.
- flohofwoe 1y agoThread-pool is just one implementation under that new async interface. There are other implementations planned which to JS-style async/await (e.g. stackless coroutines) or green threads (stackful coroutines).
- negrel 1y agoMany async implementation are still based on thread pools. rust's tokio, node's libuv uses a thread pool for file I/O. Before io_uring (which is disabled on most servers until it mature), there was no good way to do async I/O on file on Linux.
- nialv7 1y agoSure async might be implemented by threadpools, but the threadpool itself is not what we usually refer to when we talk about async as a language feature. e.g. Rust async may be backed by a threadpool, but it could also run single threaded.
- travisgriggs 1y agoI deal with async function coloring in swift and Kotlin. Have avoided it (somehow) in our Python codebase. And in Elixir, I do things on separate processes all the time, but never feel like I’m wrestling with function coloring. I do like Zig (what little I’ve played with), but continue to wish that for concurrent style computation, people would just use BEAM based languages.
- mgdev 1y agoHear hear. Elixir is a dream for this kind of stuff. But it requires very different decisions "all the way down" to make it work outside of BEAM. And BEAM itself feels heavy to most systems devs. (IMO it's not for many use cases, and to the extent it is I'm happy to see things like AtomVM start to address it.) I'm just happy I can use Elixir + Zig for NIFs.
- travisgriggs 1y agoIndeed. Zigler is tres cool.
- miki123211 1y agoHow does downstream code know which kind of asynchrony is requested / allowed? Assume some code is trying to blink an LED, which can only be done with the led_on and led_off system calls. Those system calls block until they get an ack from the LED controller, which, in the worst case, will timeout after 10s if the controller is broken. In e.g. Python, my function is either sync or async, if it's async, I know I have to go through the rigamarole of accessing the event loop and scheduling the blocking syscall on a background thread. If it's sync, I know I'm allowed to block, but I can't assume an event loop exists. In Zig, how would something like this be accomplished (assuming led_on and led_off aren't operations natively supported by the IO interface?)
- ivanjermakov 1y ago> How does downstream code know which kind of asynchrony is requested / allowed? Certainly not from the function signatures, so documentation is the only way. Seems like properly-written downstream code would deadlock if given an IO implementation that does not support concurrency. If that's true, to me this looks like a bad IO abstraction.
- jaspday 1y ago> Seems like properly written downstream code would deadlock if given an IO implementation that does not support concurrency. No, it would fail with error.ConcurrencyUnavailable. See example 10 from TFA. More to the point, this implementation decouples asynchrony from concurrency. In GP's example of a microcontroller blinking an LED, there's no explicit requirement for concurrency, only asynchrony. That is, it would be nice to be able to do other things while we wait for the LED controller to potentially time out, but it does not fundamentally change the application logic if the program blocks until the function completes. If you use io.async to run the call, then it will do exactly that: run other code while waiting for the controller if a concurrent Io implementation is passed, and block until the controller returns if a synchronous Io implementation is passed. Functions that require concurrency would, however, need to be documented. Several people have proposed adding a feature like Rust traits or C++ concepts to Zig that would enable you to annotate type signatures to indicate which features need to be available for a generic substitution to succeed, but until such a thing exists, you're pretty much best off reading the source itself. In practice, most code probably doesn't require concurrency, and library authors are free to sprinkle asynchrony everywhere without worrying about whether or not their users' Io implementation will actually support concurrency.
- geon 1y agoSo a cancelable function must poll the `cancelRequested` function, and return the error `Canceled`, right? https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1611 https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1... https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L713 https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L7...
- AndyKelley 1y agoIn the vast majority of cases, cancellation will be handled transparently by virtue of `try` being commonly used. The thing that takes relatively longer to do is I/O operations, and those will now return error.Canceled when requested. Polling cancelRequested is generally a bad idea since it introduces overhead, but you could do it to introduce cancellation points into long-running CPU tasks.
- HacklesRaised 1y agoTo the extent that it will affect ONLY my opinion of the language I reserve judgement, but prima facie, I'm a little worried that this will prove to be a distraction. If the language didn't have a std implementation of async/await, would it affect your decision to use Xig? Or are you comfortable with traditional multithreaded facilities to the extent that you require concurrency (and parallelism)?
- taccal 1y ago[dead]