frontpage.
newsnewestaskshowjobs

Made with ♥ by @iamnishanth

Open Source @Github

Open in hackernews

Zig's New Async I/O

https://kristoff.it/blog/zig-new-async-io/
156•afirium•9h ago

Comments

n42•8h ago
This is very well written, and very exciting! I especially love the implications for WebAssembly -- WASI in userspace? Bring your own IO? Why not both!
dminik•7h ago
I feel that I have to point this out once again, because the article goes so far as to state that:

> With this last improvement Zig has completely defeated function coloring.

I disagree with this. Let's look at the 5 rules referenced in the famous "What color is your function?" article referenced here.

> 1. Every function has a color

Well, you don't have async/sync/red/blue anymore, but you now have IO and non-IO functions.

> 2. The way you call a function depends on its color.

Now, technically this seems to be solved, but you still need to provide IO as a parameter. Non-IO functions don't need/take it.

It looks like a regular function call, but there's no real difference.

> 3. You can only call a red function from within another red function

This still applies. You can only call IO functions from within other IO functions.

Technically you could pass in a new executor, but is that really what you want? Not to mention that you can also do this in languages that don't claim to solve the coloring problem.

> 4. Red functions are more painful to call

I think the spirit still applies here.

> 5. Some core library functions are red

This one is really about some things being only possible to implement in the language and/or stdlib. I don't think this applies to Zig, but it doesn't apply to Rust either for instance.

Now, I think these rules need some tweaking, but the general problem behind function coloring is that of context. Your function needs some context (an async executor, auth information, an allocator, ...). In order to call such a function you also need to provide the context. Zig hasn't really solved this.

That being said, I don't think Zig's implementation here is bad. If anything, it does a great job at abstracting the usage from the implementation. This is something Rust fails at spectacularly.

However, the coloring problem hasn't really been defeated.

andyferris•7h ago
I think of it this way.

Given an `io` you can, technically, build another one from it with the same interface.

For example given an async IO runime, you could create an `io` object that is blocking (awaits every command eagerly). That's not too special - you can call sync functions from async functions. (But in JavaScript you'd have trouble calling a sync function that relies on `await`s inside, so that's still something).

Another thing that is interesting is given a blocking posix I/O that also allows for creating processes or threads, you could build in userspace a truly asynchronous `io` object from that blocking one. It wouldn't be as efficient as one based directly on iouring, and it would be old school, but it would basically work.

Going either way (changing `io` to sync or async) the caller doesn't actually care. Yes the caller needs a context, but most modern apps rely on some form of dependency injection. Most well-factored apps would probably benefit from a more refined and domain-specific "environment" (or set of platform effects, perhaps to use the Roc terminology), not Zig's posix-flavoured standard library `io` thing.

Yes rust achieves this to some extent; you can swap an async runtime for another and your app might still compile and run fine.

Overall I like this alot - I am wondering if Richard Feldmann managed to convince Andrew Kelley that "platforms" are cool and some ideas were borrowed from Roc?

dminik•6h ago
> but most modern apps rely on some form of dependency injection

Does Zig actually do anything here? If anything, this seems to be anti-Zig, where everything must be explicit.

philwelch•4h ago
Passing in your dependencies as function arguments is a form of dependency injection. It is the simplest and thus arguably best form of dependency injection.
mlugg•7h ago
The key difference to typical async function coloring is that `Io` isn't something you need specifically for asynchronicity; it's something which (unless you make a point to reach into very low-level primitives) you will need in order to perform any IO, including reading a file, sleeping, getting the time, etc. It's also just a value which you can keep wherever you want, rather than a special attribute/property of a function. In practice, these properties solve the coloring problem:

* It's quite rare for a function to unexpectedly gain a dependency on "doing IO" in general. In practice, most of your codebase will have access to an `Io`, and only leaf functions doing pure computation will not need them.

* If a function does start needing to do IO, it almost certainly doesn't need to actually take it as a parameter. As in many languages, it's typical in Zig code to have one type which manages a bunch of core state, and which the whole codebase has easy access to (e.g. in the Zig compiler itself, this is the `Compilation` type). Because of this, despite the perception, Zig code doesn't usually pass (for instance) allocators explicitly all the way down the function call graph! Instead, your "general purpose allocator" is available on that "application state" type, so you can fetch it from essentially wherever. IO will work just the same in practice. So, if you discover that a code path you previously thought was pure actually does need to perform IO, then you don't need to apply some nasty viral change; you just grab `my_thing.io`.

I do agree that in principle, there's still a form of function coloring going on. Arguably, our solution to the problem is just to color every function async-colored (by giving most or all of them access to an `Io`). But it's much like the "coloring" of having to pass `Allocator`s around: it's not a problem in practice, because you'll basically always have easy access to one even if you didn't previously think you'd need it. I think seasoned Zig developers will pretty invariably agree with the statement that explicitly passing `Allocator`s around really does not introduce function coloring annoyances in practice, and I see no reason that `Io` would be particularly different.

dminik•6h ago
> It's quite rare for a function to unexpectedly gain a dependency on ...

If this was true in general, the function coloring problem wouldn't be talked about.

However, the second point is more interesting. I think there's a bit of a Stockholm syndrome thing here with Zig programmers and Allocator. It's likely that Zig programmers won't mind passing around an extra param.

If anything, it would make sense to me to have IO contain an allocator too. Allocation is a kind of IO too. But I guess it's going to be 2 params from now on.

throwawaymaths•3h ago
> But I guess it's going to be 2 params from now on.

>> So, if you discover that a code path you previously thought was pure actually does need to perform IO, then you don't need to apply some nasty viral change; you just grab `my_thing.io

camgunz•1h ago
Python, for example, will let you call async functions inside non-async functions, you just have to set up the event loop yourself. This isn't conceptually different than the Io thing here.
Gibbon1•1h ago
I do something like that with event driven firmware. There is an allocator as part of the context. And the idea that the function is executing under some context seems fine to me.
laserbeam•21m ago
> If anything, it would make sense to me to have IO contain an allocator too. Allocation is a kind of IO too.

Io in zig is for “things that can block execution”. Things that could semantically cause a yield of any kind. Allocation is not one of those things.

Also, it’s perfectly reasonable and sometimes desireable to have 13 different allocators in your program at once. Short lived ones, long lived ones, temporary allocations, super specific allocators to optimize some areas of your game…

There are fewer reasons to want 2 different strategies to handle concurrency at the same time in your program as they could end up deadlocking on each other. Sure, you may want one in debug builds, another in release, another when running tests, but there are much fewer usecases of them running side by side.

ginko•5h ago
> It's quite rare for a function to unexpectedly gain a dependency on "doing IO" in general.

From the code sample it looks like printing to stdio will now require an Io param. So won’t you now have to pass that down to wherever you want to do a quick debug printf?

dwattttt•4h ago
I'm not familiar with Zig, but won't the classic "blocking" APIs still be around? I'd rather a synchronous debug print either way.
laserbeam•16m ago
Yes. You can always use the blocking syscalls your OS provides and ignore the Io system for stuff like that. No idea how they’d do that by default in the stdlib, but it will definitely be possible.
throwawaymaths•4h ago
std.debug.print(..) prints to stderr whuch does not need an io param.
flohofwoe•22m ago
Zig has specifically a std.debug.print() function for debug printing and a std.log module for logging. Those don't necessarily need to be wired up with the whole stdio machinery.
FlyingSnake•1h ago
> Arguably, our solution to the problem is just to color every function async-colored.

This is essentially how Golang achived color-blindness.

n42•7h ago
Aside from the ridiculous argument that function parameters color them, the assertion that you can’t call a function that takes IO from inside a function that does not is false, since you can initialize one to pass it in
dminik•7h ago
To me, there's no difference between the IO param and async/await. Adding either one causes it to not be callable from certain places.

As for the second thing:

You can do that, but... You can also do this in Rust. Yet nobody would say Rust has solved function coloring.

Also, check this part of the article:

> In the less common case when a program instantiates more than one Io implementation, virtual calls done through the Io interface will not be de-virtualized, ...

Doing that is an instant performance hit. Not to mention annoying to do.

n42•6h ago
You’re allowed to not like it, but that doesn’t change that your argument that this is a form of coloring is objectively false. I’m not sure what Rust has to do with it.
rowanG077•4h ago
Sure it is a function coloring. Just in a different form. `async` in other languages is something like an implicit parameter. In zig they made this implicit parameter explicit. Is that more better/more ergonomic? I don't know yet. The sugar is different, but the end result the same. Unless you can show me concrete example of things that the approach zig has taken can do that is not possible in say, rust. Than I don't buy that it's not just another form of function coloring.
throwawaymaths•3h ago
> Unless you can show me concrete example

add io to a struct and let the struct keep track of its own io.

gfaster•2h ago
Unless I'm misunderstanding, that's effectively implementing Future for the struct
masklinn•1h ago
It’s more like adding a runtime handle to the struct.

Modulo that I’m not sure any langage with a sync/async split has an “async” runtime built entirely out of sync operations. So a library can’t take a runtime for a caller and get whatever implementation the caller decided to use.

bmurphy1976•5h ago
>To me, there's no difference between the IO param and async/await.

You can't pass around "async/await" as a value attached to another object. You can do that with the IO param. That is very different.

jolux•2h ago
> You can't pass around "async/await" as a value attached to another object

Sure you can? You can just pass e.g. a Task around in C# without awaiting it, it's when you need a result from a task that you must await it.

kristoff_it•18m ago
yes and what does being able to await imply? that your function (the one that calls await on the Task) is an async function, otherwise your only way of getting a value out of a Task is to block on it, stealing a thread form the event loop and potentially deadlocking the entire application depending the execution context (aspnet vs desktop app, etc)

In Zig will be able to call await from any function.

MrJohz•36m ago
Sure you can. An `async` function in Javascript is essentially a completely normal function that returns a promise. The `async`/`await` syntax is a convenient syntax sugar for working with promises, but the issue would still exist if it didn't exist.

More to the point, the issue would still exist even if promises didn't exist — a lot of Node APIs originally used callbacks and a continuation-passing style approach to concurrency, and that had exactly the same issues.

throwawaymaths•4h ago
> Adding either one causes it to not be callable from certain places.

you can call a function that requires an io parameter from a function that doesn't have one by passing in a global io instance?

as a trivial example the fn main entrypoint in zig will never take an io parameter... how do you suppose you'd bootstrap the io parameter that you'd eventually need. this is unlike other languages where main might or might not be async.

ginko•3h ago
>you can call a function that requires an io parameter from a function that doesn't have one by passing in a global io instance?

How will that work with code mixing different Io implementations? Say a library pulled in uses a global Io instance while the calling code is using another.

I guess this can just be shot down with "don't do that" but it feels like a new kind of pitfall get get into.

throwawaymaths•3h ago
> Say a library pulled in uses a global Io instance while the calling code is using another.

it'll probably carry a stigma like using unsafe does.

TUSF•3h ago
Zig already has an Allocator interface that gets passed around, and the convention is that libraries don't select an Allocator. Only provide APIs that accept allocators. If there's a certain process that works best with an Arena, then the API may wrap a provided function in an Arena, but not decide on their own underlying allocator for the user.

For Zig users, adopting this same mindset for Io is not really anything new. It's just another parameter that occasionally needs to be passed into an API.

kristoff_it•28m ago
while not really idiomatic, as long as you let the user define the Io instance (eg with some kind of init function), then it doesn't really matter how that value is accessed within the library itself.

that's why this isn't really the same as async "coloring"

masklinn•1h ago
You can call an async function from a function that is not async by passing in a global runtime (/ event loop).

As a trivial example the main entry point in rust is never async. How’d you suppose you’d bootstrap the runtime that you’d eventually need.

This is pretty much like every other langage.

ayuhito•7h ago
Go also suffers from this form of “subtle coloring”.

If you’re working with goroutines, you would always pass in a context parameter to handle cancellation. Many library functions also require context, which poisons the rest of your functions.

Technically, you don’t have to use context for a goroutine and could stub every dependency with context.Background, but that’s very discouraged.

tidwall•6h ago
Context is not required in Go and I personally encourage you to avoid it. There is no shame in blazing a different path.
nu11ptr•6h ago
What would you use in its place? I've never had an issue with it. I use it for 1) early termination 2) carrying custom request metadata.

I don't really think it is fully the coloring problem because you can easily call non-context functions from context functions (but not other way around, so one way coloring issue), but you need to be aware the cancellation chain of course stops then.

schrodinger•5h ago
Why do you encourage avoiding it? Afaik it's the only way to early-abort an operation since Goroutines operate in a cooperative, not preemptive, paradigm. To be very clear, I'm asking this completely in good faith looking to learn something new!
pjmlp•4h ago
You are expecting them to actually check the value, there is nothing preemptive.

Another approach is special messages over a side channel.

deepsun•3h ago
So instead of a context you need to pass a a channel. Same problem.
pjmlp•2h ago
Not necessarily, that is one of the reasons OOP exists.

Have a struct representing the set of associated activities, owning the channel.

ozgrakkurt•1h ago
soo, a context?
ncruces•32m ago
You can take that view, yes.

But if you store your context in a struct (which is not the recommend “best practice” – but which you can do) it's no longer a function coloring issue.

I do that in on of my libraries and I feel that it's the right call.

oefrha•1h ago
The thing about context is it can be a lot more than a cancellation mechanism. You can attach anything to it—metadata, database client, logger, whatever. Even Io and Allocator if you want to. Signatures are future-proof as long as you take a context for everything.

At the end of the day you have to pass something for cooperative multitasking.

Of course it’s also trivial to work around if you don’t like the pattern, “very discouraged” or not.

zer00eyz•53m ago
> If you’re working with goroutines, you would always pass in a context parameter to handle cancellation.

The utility of context could be called a subtle coloring. But you do NOT need context at all. If your dealing with data+state (around queue and bus processing) its easy to throw things into a goroutine and let the chips fall where they will.

> which poisons the rest of your functions. You are free to use context dependent functions without a real context: https://pkg.go.dev/context#TODO

arp242•42m ago
Having all async happen completely transparently is not really logically possible. asynchronous logic is frequently fundamentally different from synchronous logic, and you need to do something different one way or the other. I don't think that's really the same as "function colouring".

And context is used for more than just goroutines. Even a completely synchronous function can (and often does) take a context, and the cancellation is often useful there too.

phplovesong•28m ago
Like you said you dont NEED context. Its just something thats available if you need it. I still think Go/Erlang has one of the best concurrency stories out there.
kristoff_it•7h ago
Here's a trick to make every function red (or blue? I'm colorblind, you decide):

    var io: std.Io = undefined;

    pub fn main() !void {
       var impl = ...;
       io = impl.io();
    }
Just put io in a global variable and you won't have to worry about coloring in your application. Are your functions blue, red or green now?

Jokes aside, I agree that there's obviously a non-zero amount of friction to using the `Io` intreface, but it's something qualitatively very different from what causes actual real-world friction around the use of async await.

> but the general problem behind function coloring is that of context

I would disagree, to me the problem seems, from a practical perspective that:

1. Code can't be reused because the async keyword statically colors a function as red (e.g. python's blocking redis client and asyncio-redis). In Zig any function that wants to do Io, be it blue (non-async) or red (async) still has to take in that parameter so from that perspective the Io argument is irrelevant.

2. Using async and await opts you automatically into stackless coroutines with no way of preventing that. With this new I/O system even if you decide to use a library that interally uses async, you can still do blocking I/O, if you want.

To me these seems the real problems of function coloring.

ismailmaj•6h ago
The global io trick would totally be valid if you’re writing an application (i.e. not a library) and don’t have use of two different implementations of io
throwawaymaths•4h ago
you could still have a library-global io, let the user set it as desired.

> use of two different implementations of io

functionally rare situation.

laserbeam•10m ago
There are plenty of libraries out there which require users to do an init() call of some sorts at startup. It is perfectly possible to design a library that only works with 1 io instance and gets it at init(). Whether people like or want that… I have no clue.
dminik•6h ago
Well, it's not really a joke. That's a valid strategy that languages use. In Go, every function is "async". And it basically blocks you from doing FFI (or at least it used to?). I wonder if Zig will run into similar issues here.

> 1. Code can't be reused because the async keyword statically colors a function

This is fair. And it's also a real pain point with Rust. However, it's funny that the "What color is your function?" article doesn't even really mention this.

> 2. Using async and await opts you automatically into stackless coroutines with no way of preventing that

This however I don't think is true. Async/await is mostly syntax sugar.

In Rust and C# it uses stackless coroutines.

In JS it uses callbacks.

There's nothing preventing you from making await suspend a green thread.

kristoff_it•6h ago
I should have specified that better, of course async and await can be lowered to different things (that's what Zig does afterall), what I wanted to say is that that's how it works in general. JS is a good counter example, but for all other mainstream languages, async means stackless coroutines (python, ruby, c#, rust, ...).

Which means that if I want to use a dependency that uses async await, it's stackless coroutines for me too whether I like it or not.

cryptonector•5h ago
> Well, you don't have async/sync/red/blue anymore, but you now have IO and non-IO functions.

> However, the coloring problem hasn't really been defeated.

Well, yes, but if the only way to do I/O were to have an Io instance to do it with then Io would infect all but pure(ish, non-Io) functions, so calling Io functions would be possible in all but those contexts where calling Io functions is explicitly something you don't want to be possible.

So in a way the color problem is lessened.

And on top of that you get something like Haskell's IO monad (ok, no monad, but an IO interface). Not too shabby, though you're right of course.

Next Zig will want monadic interfaces so that functions only have to have one special argument that can then be hidden.

throwawaymaths•4h ago
Zig's not really about hiding things but you could put it in an options struct that has defaults unless overridden at compile time.
nmilo•5h ago
The original “function colouring” blogpost has done irreparable damage to PL discussions because it’s such a stupid concept to begin with. Of course I want async functions to be “coloured” differently, they do different things! How else is a “normal function” supposed to call a function that gives you a result later——obviously you want to be forced to say what to do with the result; await it, ignore it, .then() in JS terms, etc. these are important decisions that you can’t just ignore because it’s “painful”
throwawaymaths•4h ago
> Technically you could pass in a new executor, but is that really what you want?

why does it have to be new? just use one executor, set it as const in some file, and use that one at every entrypoint that needs io! now your io doesn't propagate downwards.

jaredklewis•3h ago
So this is a tangent from the main article, but this comment made me curious and I read the original "What color is Your Function" post.

It was an interesting read, but I guess I came away confused about why "coloring" functions is a problem. Isn't "coloring" just another form of static typing? By giving the compiler (or interpreter) more meta data about your code, it can help you avoid mistakes. But instead of the usual "first argument is an integer" type meta data, "coloring" provides useful information like: "this function behaves in this special way" or "this function can be called in these kinds of contexts." Seems reasonable?

Like the author seems very perturbed that there can be different "colors" of functions, but a function that merely calculates (without any IO or side-effects) is different than one that does perform IO. A function with only synchronous code behaves very differently than one that runs code inside another thread or in a different tick of the event loop. Why is it bad to have functions annotated with this meta data? The functions behave in a fundamentally different way whether you give them special annotations/syntax or not. Shouldn't different things look different?

He mentions 2015 era Java as being ok, but as someone that’s written a lot of multithreaded Java code, it’s easy to mess up and people spam the “synchronized” keyword/“color” everywhere as a result. I don’t feel the lack of colors in Java makes it particularly intuitive or conceptually simpler.

com2kid•1h ago
> Isn't "coloring" just another form of static typing?

Yes, and so is declaring what exceptions a function can throw (checked exceptions in Java).

> Why is it bad to have functions annotated with this meta data? The functions behave in a fundamentally different way whether you give them special annotations/syntax or not. Shouldn't different things look different?

It really isn't a problem. The article makes people think they've discovered some clever gotcha when they first read it, but IMHO people who sit down for a bit and think through the issue come to the same conclusion you have - Function coloring isn't a problem in practice.

kristoff_it•9m ago
> but IMHO people who sit down for a bit and think through the issue come to the same conclusion you have - Function coloring isn't a problem in practice.

I dunno man, have you seen people complain about async virality in Rust being annoying? Have you ever tried to read a backtrace from a program that does stackless coroutines (it's not fun)? Have you seen people do basically duplicate work to maintain a blocking and an async version of the same networking library?

raincole•1h ago
> It was an interesting read, but I guess I came away confused about why "coloring" functions is a problem. Isn't "coloring" just another form of static typing?

It is. Function coloring is static typing.

But people never ever agree on what to put in typing system. For example, Java's checked exceptions are a form of typing... and everyone hates them.

Anyway it's always like that. Some people find async painful and say fuck it I'm going to manage threads manually. In the meanwhile another bunch of people work hard to introduce async to their language. Grass is always greener on the other side.

ozgrakkurt•52m ago
You are skipping the massive point here.

If you are using a library in rust, it has to be async await, tokio, send+sync and all the other crap. Or if it is sync api then it is useless for async application.

This approach of passing IO removes this problem and this is THE main problem.

This way you don’t have to use procedural macros or other bs to implement multi versioning for the functions in your library, which doesn’t work well anyway in the end.

https://nullderef.com/blog/rust-async-sync/

You can find 50 other ones like this by searching.

To be honest I don’t hope they will solve cooperative scheduling, high performance, optionally thread-per-core async soon and the API won’t be that good anyway. But hope it solves all that in the future.

dwattttt•10m ago
> Or if it is sync api then it is useless for async application.

The rest is true, but this part isn't really an issue. If you're in an async function you can call sync functions still. And if you're worried it'll block and you can afford that, I know tokio offers spawn_blocking for this purpose.

flohofwoe•37m ago
> In order to call such a function you also need to provide the context. Zig hasn't really solved this.

It is much more flexible though since you don't need to pass the IO implementation into each function that needs to do IO. You could pass it once into in init function and then use that IO impl throughout the object or module. Whether that's good style is debatable - the Zig stdlib currently has containers that take an allocator in the init function, but those are on the way out in favour of explicitly taking the allocator in each function that needs to allocate - but the user is still free to write a minimal wrapper to restore the 'pass allocator into init' behaviour.

Odin has an interesting solution in that it passes an implicit context pointer into each function, but I don't know if the compiler is clever enough to remove the overhead for called functions that don't access the context (since it also needs to peek into all called functions - theoretically Zig with it's single-compilation-unit approach could probably solve that problem better).

nurettin•5m ago
I think the point is 3 doesn't fully apply anymore. And that was the main pain point. You couldn't call a blue in red even if it didn't use IO without some kind of execution wrapper or waiter. Now you clearly can.
the__alchemist•7h ago
Et tu, Zig?
do_not_redeem•7h ago
I'm generally a fan of Zig, but it's a little sad seeing them go all in on green threads (aka fibers, aka stackful coroutines). Rust got rid of their Runtime trait (the rough equivalent of Zig's Io) before 1.0 because it performed badly. Languages and OS's have had to learn this lesson the hard way over and over again:

https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p13...

> While fibers may have looked like an attractive approach to write scalable concurrent code in the 90s, the experience of using fibers, the advances in operating systems, hardware and compiler technology (stackless coroutines), made them no longer a recommended facility.

If they go through with this, Zig will probably top out at "only as fast as Go", instead of being a true performance competitor. I at least hope the old std.fs sticks around for cases where performance matters.

dundarious•7h ago
It's hardly "all-in" if it is merely one choice of many, and the choice is made in the executable not in the library code.
do_not_redeem•7h ago
I have definitely gotten the impression that green threads will be the favored implementation, from listening to core team members and hanging around the discord. Stackless coroutines don't even exist in the language currently.
andyferris•7h ago
In the 2026 roadmap talk Andrew Kelley spoke of the fact that stackless coroutines with iouring is the end goal here (but the requires an orthogonal improvement in the compiler for inlining that data to the stack where possible).
do_not_redeem•7h ago
Do you have the timestamp? I watched that video when it came out and don't remember hearing it.
dundarious•6h ago
What does "favored" mean if event loop and direct blocking are relatively trivial and provided also/ If I can trivially use them, what do I care what Andrew or someone in core thinks? The control is all mine, and near zero cost (potential vtable indirection).

And would Rust be "all-in" if tokio was in std, so you could use its tasks everywhere? That would be a very similar level of "all-in" to Zig's current plan, but with a seemingly better API.

I understand the benefit of not being in std, but really not a fundamental issue, IMO.

geodel•4h ago
> Stackless coroutines don't even exist in the language currently.

And green thread exists in language?

kristoff_it•7m ago
green threads don't require special language support, it's just some assembly that swaps the active stack & cpu state when switching task
andyferris•7h ago
It actually has much the same benefits of Rust removing green threads and replacing them with a generic async runtime.

The point here is that "async stuff is IO stuff is async stuff". So rather than thinking of having pluggable async runtimes (tokio, etc) Zig is going with pluggable IO runtimes (which is kinda the equivalent of "which subset of libc do you want to use?").

But in both moves the idea is to remove the runtime out of the language and into userspace, while still providing a common pluggable interface so everyone shares some common ground.

mlugg•7h ago
I'm not sure how you got the perception that we're going "all in" on green threads, given that the article in OP explicitly mentions that we're hoping to have an implementation based on stackless coroutines, based on this Zig language proposal: https://github.com/ziglang/zig/issues/23446

Performance matters; we're not planning to forget that. If fibers turn out to have unacceptable performance characteristics, then they won't become a widely used implementation. Nothing discussed in this article precludes stackless coroutines from backing the "general purpose" `Io` implementation if that turns out to be the best approach.

do_not_redeem•6h ago
Does the BDFL want this though, or is it just one person's opinion that it might be nice? Given how he has been aggressively pruning proposals, I don't put any hope in them anymore unless I see some kind of positive signal from him directly.

e.g. I'd feel a lot more confident if he had made the coroutine language feature a hard dependency of the writergate refactor.

kristoff_it•6h ago
mlugg is in the Zig core team
do_not_redeem•6h ago
I'm aware, but Zig isn't a democracy where the core team votes, right? Has Andrew actually expressed that he wants the proposal? Without that we're left with scraps like this commit message where he seems ambivalent. https://github.com/ziglang/zig/commit/d6c90ceb04f8eda7c6b711...

Andrew, I know you read these threads sometimes, give us a sign so I can go down the mountain with my stone tablets and tell the people whether we'll have coroutines

kristoff_it•6h ago
we do build internal consensus before publishing articles like this one, or doing other public communication.
mlugg•6h ago
We don't know whether or not we'll have stackless coroutines; it's possible that we hit design problems we didn't foresee. However, at this moment, the general consensus is that we are interested in pursuing stackless coroutines.

While Andrew has the final say, as Loris points out, we always work to reach a consensus internally. The article lists this an an implementation that will probably exist, because we agree that it probably will; nobody is promising it, because we also agree that it isn't guaranteed.

Also, bear in mind that even if stackless coroutines don't make it into Zig, you can always use a single-threaded blocking implementation of `Io`, so you need not be negatively affected by any potential downsides to fibers either way.

This new `Io` approach has made it strictly more likely than it previously was that stackless coroutines become a part of Zig's final design.

comex•2h ago
But how will that actually work? Your stackless coroutines proposal talks about explicit primitives for defining a coroutine. But what about a function that's not designed for any particular implementation strategy - it just takes an Io and passes it on to some other functions? Will the compiler have a way to compile it as either sync or async, like apparently it did before? It would have to, if you want to avoid function colors. But your proposal doesn't explain anything about that.

Disclaimer: I'm not actually a Zig user, but I am very interested in the design space.

ksec•3h ago
That is lovely to hear. I think the general conscious is that not a single programming language has done Async right. So people are a little sceptical. But Andrew and the team so far seems to have the do it right mentality. So I guess people should be a little more optimistic.

Cant wait for 0.15 coming out soon.

nsm•2h ago
I’m confused about the assertion that green threads perform badly. 3 of the top platforms for high concurrency servers use or plan to use green threads (Go, Erlang, Java). My understanding was that green threads have limitations with C FFI which is why lower level languages don’t use them (Rust). Rust may also have performance concerns since it has other constraints to deal with.
flohofwoe•19m ago
> I'm generally a fan of Zig, but it's a little sad seeing them go all in on green threads

Read the article, you can use whatever approach you want by writing an implementation for the IO interface, green threading is just one of them.

henrikl•7h ago
Seeing a systems language like Zig require runtime polymorphism for something as common as standard IO operations seems off to me -- why force that runtime overhead on everyone when the concrete IO implementation could be known statically in almost all practical cases?
do_not_redeem•7h ago
I think it's just the Zig philosophy to care more about binary size than speed. Allocators have the same tradeoff, ArrayListUnmanaged is not generic over the allocator, so every allocation uses dynamic dispatch. In practice the overhead of allocating or writing a file will dwarf the overhead of an indirect call. Can't argue with those binary sizes.

(And before anyone mentions it, devirtualization is a myth, sorry)

kristoff_it•6h ago
> (And before anyone mentions it, devirtualization is a myth, sorry)

In Zig it's going to be a language feature, thanks to its single unit compilation model.

https://github.com/ziglang/zig/issues/23367

do_not_redeem•6h ago
Wouldn't this only work if there's only one implementation throughout the entire compliation unit? If you use 2 allocators in your app, your restricted function type has 2 possible callees for each entry, and you're back to the same problem.
Zambyte•5h ago
> A side effect of proposal #23367, which is needed for determining upper bound stack size, is guaranteed de-virtualization when there is only one Io implementation being used (also in debug builds!).

> In the less common case when a program instantiates more than one Io implementation, virtual calls done through the Io interface will not be de-virtualized, as that would imply doubling the amount of machine code generated, creating massive code bloat.

From the article

throwawaymaths•4h ago
> Wouldn't this only work if there's only one implementation throughout the entire compliation unit

in practice how often are people using more than one io in a program?

latch•2h ago
I think having a thread pool on top of some evented IO isn't _that_ uncommon.

You might have a thread pool doing some very specific thing. You can do your own threadpool which wont use the Io interface. But if one of the tasks in the threadpool wanted to read a file, I guess you'd have to pass in the blocking Io implementation.

ozgrakkurt•6h ago
It can also mean faster compilation (and sometimes better performance? https://nical.github.io/posts/rust-custom-allocators.html)

Just templating everything doesn’t mean it will be faster every time

nu11ptr•6h ago
I/O strikes me as one place where dynamic dispatch overhead would likely be negligible in practice. Obviously it depends on the I/O target and would need to be measured, but they don't call them "I/O bound" (as opposed to "CPU bound") programs for no reason.
throwawaymaths•4h ago
> why force that runtime overhead on everyone

pretty sure the intent is for systems that only use one io to have a compiler optimization that elides the cost of double indirection... but also, you're doing IO! so usually something else is the bottleneck, one extra indirection is likely to be peanuts.

ozgrakkurt•39m ago
Runtime polymorphism isn’t something inherently bad.

It is bad if you are introducing branching in a tight loop or preventing compiler from inlining things it would inline otherwise and other similar things maybe?

didibus•6h ago
I don't know Zig, but wouldn't such a change be a major breaking change where all prior Zig code doing Io wouldn't work anymore if upgraded?
open592•6h ago
Large breaking change:

https://github.com/ziglang/zig/pull/24329

xxpor•6h ago
Zig's not at 1.0 yet, so there's no stability guarantee at this point.
TUSF•4h ago
Breaking changes is just another Tuesday for Zig.
flohofwoe•14m ago
Yeah, but why is that a problem? Zig doesn't promise any stability before 1.0, and it's not like we don't need to change code in other language ecosystem frequently for all sorts of reasons (e.g. bumping a dependency version, or a new minor C/C++ compiler implementing new warnings).
sevensor•6h ago
> io.async expresses asynchronicity (the possibility for operations to happen out of order and still be correct) and it does not request concurrency, which in this case is necessary for the code to work correctly.

This is the key point for me. Regardless of whether you’re under an async event loop, you can specify that the order of your io calls does not imply sequencing. Brilliant. Separate what async means from what the io calls do.

gavinhoward•2h ago
As the author of a semi-famous post about how Zig has function colors [1], I decided to read up on this.

I see that blocking I/O is an option:

> The most basic implementation of `Io` is one that maps to blocking I/O operations.

So far, so good, but blocking I/O is not async.

There is a thread pool that uses blocking I/O. Still good so far, but blocking I/O is still not async.

Then there's green threads:

> This implementation uses `io_uring` on Linux and similar APIs on other OSs for performing I/O combined with a thread pool. The key difference is that in this implementation OS threads will juggle multiple async tasks in the form of green threads.

Okay, they went the Go route on this one. Still (sort of) not async, but there is an important limitation:

> This implementation requires having the ability to perform stack swapping on the target platform, meaning that it will not support WASM, for example.

But still no function colors, right?

Unfortunately not:

> This implementation [stackless coroutines] won’t be available immediately like the previous ones because it depends on reintroducing a special function calling convention and rewriting function bodies into state machines that don’t require an explicit stack to run.

(Emphasis added.)

And the function colors appear again.

Now, to be fair, since there are multiple implementation options, you can avoid function colors, especially since `Io` is a value. But those options are either:

* Use blocking I/O.

* Use threads with blocking I/O.

* Use green threads, which Rust removed [2] for good reasons [3]. It only works in Go because of the garbage collector.

In short, the real options are:

* Block (not async).

* Use green threads (with their problems).

* Function colors.

It doesn't appear that the function colors problem has been defeated. Also, it appears to me that the Zig team decided to have every concurrency technique in the hope that it would appear innovative.

[1]: https://gavinhoward.com/2022/04/i-believe-zig-has-function-c...

[2]: https://github.com/aturon/rfcs/blob/remove-runtime/active/00...

[3]: https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p13...

ozgrakkurt•35m ago
Their bet seems to be that they can transparently implement real async inside an IO implementation using compiler magic. But then it means if you use that IO instance with the magic then your function gets transformed into a state machine?

Then this whole thing is useless for implementing cooperative scheduling async like in rust?

flohofwoe•8m ago
> But then it means if you use that IO instance with the magic then your function gets transformed into a state machine?

This was essentially like the old async/await implementation in Zig already worked. The same function gets the state-machine treatment if it was called in an async context, otherwise it's compiled as a 'regular' sequential function.

E.g. at runtime there may be two versions of a function, but not in the code base. Not sure though how that same idea would be implemented with the new IO interface, but since Zig strictly uses a single-compilation-unit model the compiler might be able to trace the usage of a specific IO implementation throughout the control flow?

eestrada•2h ago
Although I'm not wild about the new `io` parameter popping up everywhere, I love the fact that it allows multiple implementations (thread based, fiber based, etc.) and avoids forcing the user to know and/or care about the implementation, much like the Allocator interface.

Overall, I think it's a win. Especially if there is a stdlib implementation that is a no overhead, bogstock, synchronous, blocking io implementation. It follows the "don't pay for things you don't use" attitude of the rest of zig.

ozgrakkurt•47m ago
Isn’t “don’t pay for what you don’t use” a myth? Some other person will using unless you are a very small team with discipline, and you will pay for it.

Or just passing around an “io” is more work than just calling io functions where you want them.

aatd86•1h ago
So is that zig becoming a type AND effect system?
phplovesong•32m ago
I wish Zig had not done async/await. CPS (like you have in Go) is way, way better, and is lower level, making it possible to do you own "async/await" if you really want to.
flohofwoe•28m ago
Read the article, the new Zig async/await interface doesn't imply the typical async/await state-machine code transformation. You can write a simple blocking runtime, or a green-thread implementation, or a thread-pool, or the state-machine approach via stackless coroutines (but AFAIK this needs a couple of language builtins which then must be implemented in an IO implementation).
osa1•16m ago
By CPS do you mean lightweight threads + meeting point channels? (i.e. both the reader and writer get blocked until they meet at the read/write call) Or something else?

Why is CPS better and lower level than async/await?

noelwelsh•21m ago
Ok, they are implementing an effect system. Is there any acknowledgement that they are going down an established path?

Bypassing Google's big anti-adblock update

https://0x44.xyz/blog/web-request-blocking/
647•deryilz•13h ago•552 comments

Switching to Claude Code and VSCode Inside Docker

https://timsh.org/claude-inside-docker/
96•timsh•1d ago•43 comments

Zig's New Async I/O

https://kristoff.it/blog/zig-new-async-io/
157•afirium•9h ago•112 comments

Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model

https://github.com/MoonshotAI/Kimi-K2
331•ConteMascetti71•14h ago•79 comments

MacPaint Art from the Mid-80s Still Looks Great Today

https://blog.decryption.net.au/posts/macpaint.html
869•decryption•23h ago•179 comments

Aeron: Efficient reliable UDP unicast, UDP multicast, and IPC message transport

https://github.com/aeron-io/aeron
17•todsacerdoti•11h ago•4 comments

Hacking Coroutines into C

https://wiomoc.de/misc/posts/hacking_coroutines_into_c.html
76•jmillikin•7h ago•22 comments

Chrome's hidden X-Browser-Validation header reverse engineered

https://github.com/dsekz/chrome-x-browser-validation-header
159•dsekz•2d ago•34 comments

Experimental imperative-style music sequence generator engine

https://github.com/renoise/pattrns
13•bwidlar•3d ago•1 comments

Parse, Don't Validate (For C)

https://www.lelanthran.com/chap13/content.html
51•lelanthran•4d ago•12 comments

C++: Maps on Chains

http://bannalia.blogspot.com/2025/07/maps-on-chains.html
7•signa11•2d ago•3 comments

Edward Burtynsky's monumental chronicle of the human impact on the planet

https://www.newyorker.com/culture/photo-booth/earths-poet-of-scale
37•pseudolus•5h ago•5 comments

Lost Chapter of Automate the Boring Stuff: Audio, Video, and Webcams in Python

https://inventwithpython.com/blog/lost-av-chapter.html
152•AlSweigart•15h ago•9 comments

Programming Affordances That Invite Mistakes

https://thetechenabler.substack.com/p/programming-affordance-when-a-languages
23•ingve•2d ago•7 comments

ISRO successfully conducts hot tests of Gaganyaan propulsion system

https://www.thehindu.com/sci-tech/science/isro-successfully-conducts-hot-tests-of-gaganyaan-propulsion-system/article69790839.ece
3•Bluestein•4m ago•0 comments

Light exposure at night predicts incidence of cardiovascular diseases

https://www.medrxiv.org/content/10.1101/2025.06.20.25329961v1
106•gnabgib•9h ago•74 comments

The fish kick may be the fastest subsurface swim stroke yet (2015)

https://nautil.us/is-this-new-swim-stroke-the-fastest-yet-235511/
213•bookofjoe•19h ago•141 comments

Two-step system makes plastic from carbon dioxide, water and electricity

https://phys.org/news/2025-06-plastic-carbon-dioxide-electricity.html
59•PaulHoule•3d ago•15 comments

A better Ghidra MCP server – GhidrAssistMCP

https://github.com/jtang613/GhidrAssistMCP
80•jtang613•14h ago•15 comments

Show HN: I made a JSFiddle-style playground to test and share prompts fast

https://langfa.st/
31•eugenegusarov•14h ago•3 comments

Bayeux Tapestry Will Return to the U.K. In 950 Years

https://news.artnet.com/art-world/bayeux-tapestry-british-museum-loan-2665313
8•andsoitis•1d ago•4 comments

Second Variety, by Philip K. Dick (1953)

https://www.gutenberg.org/files/32032/32032-h/32032-h.htm
62•djoldman•3d ago•19 comments

HNSW as abstract data structure: video intro to Redis vector sets

https://www.youtube.com/watch?v=kVApsFUeuEA
25•antirez•2d ago•0 comments

Malware found in official gravityforms plugin indicating supply chain breach

https://patchstack.com/articles/critical-malware-found-in-gravityforms-official-plugin-site/
214•taubek•1d ago•43 comments

New Date("wtf") – How well do you know JavaScript's Date class?

https://jsdate.wtf
322•OuterVale•1d ago•187 comments

Working through 'Writing A C Compiler'

https://jollygoodsw.wordpress.com/2025/03/13/working-through-writing-a-c-compiler/
143•AlexeyBrin•19h ago•33 comments

Supreme Court's ruling practically wipes out free speech for sex writing online

https://ellsberg.substack.com/p/free-speech
571•macawfish•14h ago•753 comments

Exposing a web service with Cloudflare Tunnel (2022)

https://erisa.dev/exposing-a-web-service-with-cloudflare-tunnel/
96•sturza•3d ago•40 comments

Vibe-Coding a PCB – surprisingly good

https://atomic14.substack.com/p/vibe-coding-a-pcb-surprisingly-good
148•iamflimflam1•16h ago•60 comments

Proposed NOAA Budget Kills Program Designed to Prevent Satellite Collisions

https://skyandtelescope.org/astronomy-news/proposed-noaa-budget-kills-program-to-prevent-satellite-collisions/
335•bikenaga•15h ago•197 comments