Is this in effect introducing algebraic effects by concept? E.g. the io passed in is an effect handler, and it is the effect handler's choice whether to perform stack switching (or other means of non-blocking waiting) to enable asynchronicity?
Zig's New Async I/O
141–150 of 293 posts
Re: Zig's New Async I/O
#142Earlier quoted context omitted.
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 throug…
>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.
If you have a sync/non-IO function that now needs to do IO, it becomes async/IO. And since IO and async are viral, it's callers must also now be IO/async and call it with IO/await. All the way up the call stack.
Re: Zig's New Async I/O
#143Earlier quoted context omitted.
soo, a context?
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 (for that library).
An exception to the short-lived rule is to put context in your service struct and pass it as the base context when constructing the HTTP server, so that when you get a service shutdown signal, one can cancel requests gracefully.
Re: Zig's New Async I/O
#144Earlier quoted context omitted.
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 throug…
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.
I don't have a problem with IO conceptually (but I do have a problem with Zig ergonomics, allocator included). I do have a problem with claiming you defeated function coloring.
Like, look. You didn't even get rid of await ...
> try a_future.await(io);
Re: Zig's New Async I/O
#145As 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 implem…
This is an internal implementation detail rather than a fact which is usually exposed to the user. This is essentially just explaining that the Zig compiler needs to figure out which functions are async and lower them differently.
We do have an explicit calling convention, `CallingConvention.async`. This was necessary in the old implementation of async functions in order to make runtime function pointer calls work; the idea was that you would cast your `fn () void` to a `fn () callconv(.async) void`, and then you could call the resulting `*const fn () callconv(.async) void` at runtime with the `@asyncCall` builtin function. This was one of the biggest flaws in the design; you could argue that it introduced a form of coloring, but in practice it just made vtables incredibly undesirable to use, because (since nobody was actually doing the `@asyncCall` machinery in their vtable implementations) they effectively just didn't support async.
We're solving this with a new language feature [0]. The idea here is that when you have a virtual function -- for a simple example, let's say `alloc: *const fn (usize) ?[*]u8` -- you instead give it a "restricted function pointer type", e.g. `const AllocFn = @Restricted(*const fn (usize) ?[*]u8);` with `alloc: AllocFn`. The magic bit is that the compiler will track the full set of comptime-known function pointers which are coerced to `AllocFn`, so that it can know the full set of possible `alloc` functions; so, when a call to one is encountered, it knows whether or not the callee is an async function (in the "stackless async" sense). Even if some `alloc` implementations are async and some are not, the compiler can literally lower `vtable.alloc(123)` to `switch (vtable.alloc) { impl1 => impl1(123), impl2 => impl2(123), ... }`; that is, it can look at the pointer, and determine from that whether it needs to dispatch a synchronous or async call.
The end goal is that most function pointers in Zig should be used as restricted function pointers. We'll probably keep normal function pointers around, but they ideally won't be used at all often. If normal function pointers are kept, we might keep `CallingConvention.async` around, giving a way to call them as async functions if you really want to; but to be honest, my personal opinion is that we probably shouldn't do that. We end up with the constraint that unrestricted pointers to functions where the compiler has inferred the function as async (in a stackless sense) cannot become runtime-known, as that would lead to the compiler losing track of the calling convention it is using internally. This would be a very rare case provided we adequately encourage restricted function pointers. Hell, perhaps we'd just ban all unrestricted default-callconv function pointers from becoming runtime-known.
Note also that stackless coroutines do some with a couple of inherent limitations: in particular, they don't play nicely with FFI (you can't suspend across an FFI boundary; in other words, a function with a well-defined calling convention like the C calling convention is not allowed to be inferred as async). This is a limitation which seems perfectly acceptable, and yet I'm very confident that it will impact significantly more code than the calling convention thing might.
TL;DR: depending on where the design ends up, the "calling convention" mentioned is either entirely, or almost entirely, just an implementation detail. Even in the "almost entirely" case, it will be exceptionally rare for anyone to write code which could be affected by it, to the point that I don't think it's a case worth seriously worrying about unless it proves itself to actually be an issue in practice.
Re: Zig's New Async I/O
#146Re: Zig's New Async I/O
#147Earlier quoted context omitted.
> 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…
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.
Re: Zig's New Async I/O
#148I 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 fun…
> If anything, it does a great job at abstracting the usage from the implementation. This is something Rust fails at spectacularly. Could you expand on this? I don't get what you mean
Async hasn't been added yet, so you're using `std::net::TcpStream`.
All is well until async comes along. Now, you have a problem. If you use async, your previous sync users won't be able to (easily) call your functions. You're looking at an API redesign.
So, you swallow your pride and add an async variant of your functionality. Since Tokio is most popular, you use `tokio::net::TcpStream`.
All is well, until a user comes in and says "Hey, I would like to use your library with smol (a different async runtime)". Now what do you do? Add a third variant of your code using `smol::net::TcpStream`? It's getting a bit ridiculous, and smol isn't the only alternative runtime.
One solution is to do what Zig does, but there isn't really an agreed upon solution. The stdlib does not even provide AsyncRead/AsyncWrite so you could invert your code and just work with streams provided from above and keep your libary executor agnostic.
Re: Zig's New Async I/O
#149Re: Zig's New Async I/O
#150Earlier quoted context omitted.
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 th…
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 fun…
The idea is that, yes, the compiler will infer whether or not a function is async (in the stackless async sense) based on whether it has any "suspension point", where a suspension point is either: * Usage of `@asyncSuspend` * A call to another async function
Calls through function pointers (where we typically wouldn't know what we're calling, and hence don't know whether or not it's async!) are handled by a new language feature which has already been accepted; see a comment I left a moment ago [1] for details on that.
If the compiler infers a function to be async, it will lower it differently; with each suspension point becoming a boundary where any stack-local state is saved to the async frame, as well as an integer indicating where we are in the function, and we jump to different code to be resumed once it finishes. The details of this depend on specifics of the proposal (which I'm planning to change soon) and sometimes melt my brain a little, so I'll leave them unexplained for now, but can probably elaborate on them in the issue thread at some point.
Of course, this analysis of whether a function is async is a little bit awkward, because it is a whole-program analysis; a change in a leaf function in a little file in a random helper module could introduce asynchronocity which propagates all the way up to your `pub fn main`. As such, we'll probably have different strategies for this inference in the compiler depending on the release mode:
* In Debug mode, it may be a reasonable strategy to just assume that (almost) all functions are asynchronous (it's safe to lower a synchronous function as asynchronous, just not vice versa). The overhead introduced by the async lowering will probably be fairly minimal in the context of a Debug build, and this will speed up build times by allowing functions to be sent straight to the code generator (like they are today) without having to wait for other functions to be analyzed (and without potentially having to codegen again later if we "guessed wrong").
* In Release[Fast,Small,Safe] mode, we might hold back code generation until we know for sure, based on the parts of the call graph we have analyzed, whether or not a function is async. Vtables might be a bit of a problem here, since we don't know for sure that a vtable call is not async until we've finished literally all semantic analysis. Perhaps we'll make a guess about whether such functions are async and re-do codegen later if that guess was wrong. Or, in the worst case... perhaps we'll literally just defer all codegen until semantic analysis completes! After all, it's a release build, so you're going to be waiting a while for optimizations anyway; you won't mind an extra couple of seconds on delayed codegen.