Live data from Hacker News

Zig's new plan for asynchronous programs

lwn.net

121–130 of 274 posts

Re: Zig's new plan for asynchronous programs

#121

Is there any way to implement structured concurrency on top of the std.Io primitive?

    var group: Io.Group = .init;
    defer group.cancel(io);
If you see this pattern, you are doing structured concurrency.

Same thing with:

    var future = io.async(foo, .{});
    defer future.cancel(io);

Re: Zig's new plan for asynchronous programs

#122

I think this design is very reasonable. However, I find Zig's explanation of it pretty confusing: they've taken pains to emphasize that it solves the function coloring problem, which it doesn't: it pushes I/O into an effect type, which essentially behaves as a token that callers need to retain. This is a form of coloring, albeit one that's much more ergonomic. (To my understanding this is pretty similar to how Go sol…

Function coloring is specifically about requiring syntax for a function, eg. the async keyword. So if you want an async and non-async function you need to write both in code. If you pass the "coloring" as an argument you avoid the need for extra syntax and multiple function definitions and therefor the function has no color. You can solve this in various ways with various tradeoffs but as long as there is a single function (syntactically) is all that matters for coloring.

Re: Zig's new plan for asynchronous programs

#123
post #79

This design seems very similar to async in scala except that in scala the execution context is an implicit parameter rather than an explicit parameter. I did not find this api to be significantly better for many use cases than writing threads and communicating over a concurrent queue. There were significant downsides as well because the program behavior was highly dependent on the execution context. It led to spooky…

> I did not find this api to be significantly better for many use cases than writing threads and communicating over a concurrent queue.

The problem with using OS threads, you run into scaling problems due to Little's law. On the JVM we can use virtual threads, which don't run into that limitation, but the JVM can implement user-mode threads more efficiently than low-level languages can for several reasons (the JIT can see through all virtual calls, the JVM has helpful restrictions on pointers into the stack, and good GCs make memory management very cheap in exchange for a higher RAM footprint). So if you want scalability, low-level languages need other solutions.

Re: Zig's new plan for asynchronous programs

#124
post #74
post #62

One thing the old Zig async/await system theoretically allowed me to do, which I'm not certain how to accomplish with this new io system without manually implementing it myself, is suspend/resume. Where you could suspend the frame of a function and resume it later. I've held off on taking a stab at OS dev in Zig because I was really, really hoping I could take advantage of that neat feature: configure a device or sub…

Can you create a thread pool consisting of one thread, and suspend / resume the thread?

Doesn't that negate the point of using coroutines? light-weight concurrency

Re: Zig's new plan for asynchronous programs

#125

I think this design is very reasonable. However, I find Zig's explanation of it pretty confusing: they've taken pains to emphasize that it solves the function coloring problem, which it doesn't: it pushes I/O into an effect type, which essentially behaves as a token that callers need to retain. This is a form of coloring, albeit one that's much more ergonomic. (To my understanding this is pretty similar to how Go sol…

Function coloring is specifically about requiring syntax for a function, eg. the async keyword. So if you want an async and non-async function you need to write both in code. If you pass the "coloring" as an argument you avoid the need for extra syntax and multiple function definitions and therefor the function has no color. You can solve this in various ways with various tradeoffs but as long as there is a single fu…

> Function coloring is specifically about requiring syntax for a function, eg. the async keyword.

It isn't really. It's about having two classes of functions (async and sync), and not being able to await async functions from sync ones.

It was originally about Javascript, where it is the case due to how the runtime works. In a sync function you can technically call an async one, but it returns a promise. There's no way to get the actual result before you return from your sync function.

That isn't the case for all languages though. E.g. in Rust: https://docs.rs/futures/latest/futures/executor/fn.block_on....

I think maybe Python can do something similar but don't quote me on that.

There's a closely related problem about making functions generic over synchronicity, which people try and solve with effects, monads, etc. Maybe people call that "function colouring" now, but that wasn't exactly the original meaning.

Re: Zig's new plan for asynchronous programs

#126
post #83

Earlier quoted context omitted.

just pass around handles like you do in zig, alright? also: spawn_blocking for blocking code

But that's the thing, idiomatic Rust sync code almost never passes around handles, even when they need to do I/O. You might be different, and you might start doing that in your code, but almost none of either std or 3rd party libraries will cooperate with you. The difference with Zig is not in its capabilities, but rather in how the ecosystem around its stdlib is built. The equivalent in Rust would be if almost all I…

> But that's the thing, idiomatic Rust sync code almost never passes around handles, even when they need to do I/O.

Because they don't use async inside.

Zig code is passing around handles in code without io?

Re: Zig's new plan for asynchronous programs

#127
post #81

Earlier quoted context omitted.

So do Python and Javascript. I think most languages with async/await also support noop-ing the yield if the future is already resolved. It’s only when you create a new task/promise that stuff is guaranteed to get scheduled instead of possibly running immediately.

I can't quite parse what you're saying. Python works like this: import asyncio async def sleepy() -> None: print('Sleepy started') await asyncio.sleep(0.25) print('Sleepy resumed once') await asyncio.sleep(0.25) print('Sleepy resumed and is done!') async def main(): sleepy_future = sleepy() print('Started a sleepy') await asyncio.sleep(2) print('Main woke back up. Time to await the sleepy.') await sleepy_future if __…

That’s exactly the behavior I’m describing.

`sleepy_future = sleepy()` creates the state machine without running anything, `create_task` actually schedules it to run via a queue, `asyncio.sleep` suspends the main task so that the newly scheduled task can run, and `await sleepy_task` either yields the main task until sleepy_task can finish, or no-ops immediately if it has already finished without yielding the main task.

My original point is that last bit is a very common optimization in languages with async/await since if the future has already resolved, there’s no reason to suspend the current task and pay the switching overhead if the task isn’t blocked waiting for anything.

Re: Zig's new plan for asynchronous programs

#128
post #126

Earlier quoted context omitted.

But that's the thing, idiomatic Rust sync code almost never passes around handles, even when they need to do I/O. You might be different, and you might start doing that in your code, but almost none of either std or 3rd party libraries will cooperate with you. The difference with Zig is not in its capabilities, but rather in how the ecosystem around its stdlib is built. The equivalent in Rust would be if almost all I…

> But that's the thing, idiomatic Rust sync code almost never passes around handles, even when they need to do I/O. Because they don't use async inside. Zig code is passing around handles in code without io?

> Because they don't use async inside.

But they use I/O inside, and we arrive at this issue:

I'm writing async, and I need to call std::fs::read. I can't, because it blocks the thread; I could use spawn_blocking but that defeats the purpose of async. So instead I have to go look for a similar function but of the other color, probably from tokio.

In Zig, if you're writing sync, you call the standard library function for reading files. If you're writing async, you call the same library function for reading files. Then, the creator of the `io` object decides whether the whole thing will be sync or async.

Re: Zig's new plan for asynchronous programs

#129
post #115

Earlier quoted context omitted.

In the JS example, a synchronous function cannot poll the result of a Promise. This is meaningfully different when implementing loops and streams. Ex, game loop, an animation frame, polling a stream. A great example is React Suspense. To suspend a component, the render function throws a Promise. To trigger a parent Error Boundary, the render function throws an error. To resume a component, the render function returns…

I see. I guess JS is the only language with the coloring problem, then, which is strange because it's one of the few with a built-in event loop. This Io business is isomorphic to async/await in Rust or Python [1]. Go also has a built-in "event loop"-type thing, but decidedly does not have a coloring problem. I can't think of any languages besides JS that do. [1]: https://news.ycombinator.com/item?id=46126310

> Go also has a built-in "event loop"-type thing, but decidedly does not have a coloring problem.

context is kind of a function color in go, and it's also a function argument.

Re: Zig's new plan for asynchronous programs

#130
post #98

Earlier quoted context omitted.

It's not a monad because it doesn't return a description of how to carry out I/O that is performed by a separate system; it does the I/O inside the function before returning. That's a regular old interface, not a monad.

So it's the reader monad, then? ;-)

Yes.
Post reply on HN