-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
introduce noasync
keyword to annotate functions, function calls, and awaits
#3157
Comments
Checklist:
|
I think that I instead propose that we solve this in userland with a linked list of event loops; so that event loops can be nested or really: created from anywhere. Which includes the inside of e.g. the stack trace handler. const LoopList = std.SinglyLinkedList(void);
pub const Loop = struct {
parent: LoopList.Node,
....
}
/// A global containing the current event loop
var currentLoop = LoopList.init();
pub fn newLoop() Loop {
const loop = Loop.new(); // could create different loop types; e.g. thread pool based vs uring based vs poll() based vs epoll() based.
currentLoop.prepend(loop);
return loop;
}
// example 'blocking' read function
fn read(fd: int, dest: []u8) !usize {
var popLoop: bool = undefined;
var loop: Loop = undefined;
if (currentLoop.first) |l| {
loop = l;
popLoop = false;
} else {
loop = newLoop();
popLoop = true;
}
defer {
if (popLoop) {
loop.close();
currentLoop.popFirst();
}
}
return loop.read(fd, dest);
} By allowing event loops to nest, you can enable some really cool patterns. |
I think your nested event loop example is going to be possible and it is an independent concept from this I do have some questions about this example, such as how does it interact with threads? Let's have another issue open for this use case. |
Why? Removing the seam was the point of #1778: you're introducing a "colour" back to functions. |
The different "colors" for functions are still there in Zig right now. They are inferred, which It is inferred that a function is async, if its implementation uses the For
TLDR: |
|
That to me is the design feature: if you cannot tell, then people can't intentionally create single-coloured functions. |
@daurnimator what's your plan for making |
An event loop would only Note that "event loop" here is used a bit loosely: there is no event loop here; a better description might be "async scheduler" |
at compile time or runtime? If runtime, then the function will be generated async. Which will bubble all the way up to |
This introduces the concept of "IO mode" which is configurable by the root source file (e.g. next to `pub fn main`). Applications can put this in their root source file: ``` pub const io_mode = .evented; ``` This will populate `std.io.mode` to be `std.io.Mode.evented`. When I/O mode is evented, `std.os.read` handles EAGAIN by suspending until the file descriptor becomes available for reading. Although the std lib event loop supports epoll, kqueue, and Windows I/O Completion Ports, this integration with `std.os.read` currently only works on Linux. This integration is currently only hooked up to `std.os.read`, and not, for example, `std.os.write`, child processes, and timers. The fact that we can do this and still have a working master branch is thanks to Zig's lazy analysis, comptime, and inferred async. We can continue to make incremental progress on async std lib features, enabling more and more test cases and coverage. In addition to `std.io.mode` there is `std.io.is_async` which is equal to `std.io.mode == .evented`. In case I/O mode is async, `std.io.InStream` notices this and the read function pointer becomes an async function pointer rather than a blocking function pointer. Even in this case, `std.io.InStream` can *still be used as a blocking input stream*. Users of the API control whether it is blocking or async at runtime by whether or not the read function suspends. In case of file descriptors, for example, this might correspond to whether it was opened with `O_NONBLOCK`. The `noasync` keyword makes a function call or `await` assert that no suspension happens. This assertion has runtime safety enabled. `std.io.InStream`, in the case of async I/O, uses by default a 4 MiB frame size for calling the read function. If this is too large or too small, the application can globally increase the frame size used by declaring `pub const stack_size_std_io_InStream = 1234;` in their root source file. This way, `std.io.InStream` will only be generated once, avoiding bloat, and as long as this number is configured to be high enough, everything works fine. Zig has runtime safety to detect when `@asyncCall` is given too small of a buffer for the frame size. This merge introduces -fstack-report which can help identify large async function frame sizes and explain what is making them so big. Until #3069 is solved, it's recommended to stick with blocking IO mode. -fstack-report outputs JSON format, which can then be viewed in a GUI that represents the tree structure. As an example, Firefox does a decent job of this. One feature that is currently missing is detecting that the call stack upper bound is greater than the default for a given target, and passing this upper bound to the linker. As an example, if Zig detects that 20 MiB stack upper bound is needed - which would be quite reasonable - currently on Linux the application would only be given the default of 16 MiB. Unrelated miscellaneous change: added std.c.readv
I was messing with this some today and have a few thoughts. Since the event loop is global, could I just grab the instance and run some async fn like? var result = loop.runUntilComplete(someframe); This is somewhat like @daurnimator's embedded event loops. Where it would just sit there in a while loop that keeps ticking until the frame is complete. Or would it be possible to switch between sync and async "versons" at runtime, using some thread-local variable? I started making sync only alternative fn's but that quickly becomes a mess. |
Note that there is not yet runtime safety for this. See #3157
Superseded by #4696. |
I got pretty far in the proof-of-concept branch for adding a global
pub const io_mode = .evented;
. Here's one issue that came up (text version follows after screenshot):This is really interesting!
Creating the event loop in main() didn't work, because it calls
eventfd
, and if it gets an unexpected OS error (in debug builds), it tries to dump a stack trace to pinpoint where the unexpected OS error occurred- which wants to open the self exe file to read dwarf info, which callsstd.os.read()
, which is getting generated event-based, because the application has selectedpub const io_mode = .evented;
but we can't suspend here because this is setting up the event loop itself.What do we actually want to happen here? Answer: dumping the current stack trace should always be blocking and should not depend on an event loop. And we can accomplish this in a clean way:
Even if a function is async, one can make it be blocking if all the I/O it does is on file descriptors which are not
O_NONBLOCK
, becauseasync func()
runsfunc()
up to the first suspend point. If the I/O it does is all blocking it will finish completely without suspending.So we will make
std.debug.dumpCurrentStackTrace
always be a non-async function. This is accomplished by opening debug info with normal blocking file descriptor, and then do an async call for the async functions it calls, and then assert that they all finished without suspending. This makesdumpCurrentStackTrace
a "seam". Even though it calls async functions, it knows that, in this case, they will return without suspending, and so it ends up being a non-async function.This is elegant because all those async functions are not generated twice. The async versions of the functions can be used for both the blocking and the non-blocking path.
Without introducing any new syntax or language semantics, here's what this would look like:
This is obviously less than ideal, and it generates worse code than what I am proposing:
This keyword annotates a function call and guarantees that the function call will not be a suspension point, even if the callee is an async function. It asserts that the callee finished (got to the return statement).
Similarly,
noasync
could be used in front ofawait
. This diff would be equivalent:Finally, considering that the main use case for this feature is to make a function be a "seam" between non-async and async code, it would make sense for
noasync
to be able to annotate a function directly. This would cause all function calls andawait
within the function body into being the "noasync" versions of them. It would also cause other suspension points (such assuspend
) to be a compile error.With this implemented, the solution to the above compile error would be a single line:
This has the added benefit of providing semantically meaningful documentation for the function. It's useful for the callers to know whether a function has this attribute.
The text was updated successfully, but these errors were encountered: