Andrew 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.
I 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.
I 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.
have 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.
I don't think any language that's not well-established, let alone one that isn't stabilised yet, would have a strong selling point if what you're looking for right now is to write production code that you'll maintain for a decade or more to come (I mean, companies do use languages that aren't as established as C in production apps, including Zig, but that's certainly not for everyone).
But if you're open to learning languages for tinkering/education purposes, I would say that Zig has several significant "intrinsic" advantages compared to C.
* It's much more expressive (it's at least as expressive as C++), while still being a very simple language (you can learn it fully in a few days).
* Its cross-compilation tooling is something of a marvel.
* It offers not only spatial memory safety, but protection from other kinds of undefined behaviour, in the form of things like tagged unions.
> 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.
It 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.
I 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).
I 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.
TL;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.
It'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.
Oh 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.
hard for me to argue with your point but if the understanding is that cancellation causes early return of the function, then i suppose the signature is, eerrr, consistent?
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).
I 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.
Hear 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.)
How 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?)
> 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.
In 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.
To 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)?
32 comments
[ 6.4 ms ] story [ 61.7 ms ] threadAsync 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.
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.
But if you're open to learning languages for tinkering/education purposes, I would say that Zig has several significant "intrinsic" advantages compared to C.
* It's much more expressive (it's at least as expressive as C++), while still being a very simple language (you can learn it fully in a few days).
* Its cross-compilation tooling is something of a marvel.
* It offers not only spatial memory safety, but protection from other kinds of undefined behaviour, in the form of things like tagged unions.
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#L1...
Text version.
The desynced video makes the video a bit painful to watch.
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.
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.
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...
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.
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).
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
(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.
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?)
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.
https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1...
https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L7...
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.
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)?