-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Proposal: a way to check async frame liveness #3164
Comments
Here's what that "async/await typical usage" example would look like with this (I took the liberty of making it a property of a fn asyncAwaitTypicalUsage(allocator: *Allocator) !void {
var download_frame = async fetchUrl(allocator, "https://example.com/");
errdefer if (download_frame.pending) {
if (await download_frame) |x| allocator.free(x) else |_| {}
};
var file_frame = async readFile(allocator, "something.txt");
errdefer if (file_frame.pending) {
if (await file_frame) |x| allocator.free(x) else |_| {}
};
const download_text = try await download_frame;
defer allocator.free(download_text);
const file_text = try await file_frame;
defer allocator.free(file_text);
expect(std.mem.eql(u8, "expected download text", download_text));
expect(std.mem.eql(u8, "expected file text", file_text));
}
Indeed, this is problematic. Even the I'll create a sister proposal for fn asyncAwaitTypicalUsage(allocator: *Allocator) !void {
var download_frame = async fetchUrl(allocator, "https://example.com/");
errdefer cancel download_frame;
var file_frame = async readFile(allocator, "something.txt");
errdefer cancel file_frame;
const download_text = try await download_frame;
defer allocator.free(download_text);
const file_text = try await file_frame;
defer allocator.free(file_text);
expect(std.mem.eql(u8, "expected download text", download_text));
expect(std.mem.eql(u8, "expected file text", file_text));
} The main problem to solve with that proposal is making it work with non-async functions. (related: #782). |
An important scenario that doesn't get enough attention from Zig's async, I think, is when time management is important, such as when making a game engine. |
After playing around with async/await in other languages and looking into how they're implemented, I definitely think allowing for a way to check an async frames liveness will help simplify a lot of async-related code. By enabling the ability to check if an async frame is completed, it's trivial to implement operations such as 'await until either frame A or B is completed', or 'await until all frames {A, B, C, ...} are completed'. Here's an example of how they would be implemented: const std = @import("std");
// A hack to check if a frame is completed. I don't know if
// this actually works for all kinds of async frames.
pub fn completed(frame: anytype) bool {
const bytes = std.mem.asBytes(frame);
return std.mem.allEqual(u8, bytes[8..][0..16], std.math.maxInt(u8));
}
fn ReturnTypeOf(comptime function: anytype) type {
return @typeInfo(@TypeOf(function)).Fn.return_type.?;
}
fn suspended() void {
suspend;
}
const All = struct {
state: @Frame(suspended),
counter: usize,
pub fn init() All {
var self: All = undefined;
self.state = async suspended();
self.counter = 0;
return self;
}
pub fn run(self: *All, comptime function: anytype, args: anytype) ReturnTypeOf(function) {
// 'self.counter' could be made thread-safe.
self.counter += 1;
const result = @call(.{}, function, args);
self.counter -= 1;
if (self.counter == 0 and !completed(&self.state)) {
resume self.state;
}
return result;
}
pub fn wait(self: *All) void {
await self.state;
}
};
const Either = struct {
state: @Frame(suspended),
pub fn init() Either {
var self: Either = undefined;
self.state = async suspended();
return self;
}
pub fn run(self: *Either, comptime function: anytype, args: anytype) ReturnTypeOf(function) {
const result = @call(.{}, function, args);
if (!completed(&self.state)) {
resume self.state;
}
return result;
}
pub fn wait(self: *Either) void {
await self.state;
}
};
test "Either" {
var either = Either.init();
var F1 = async either.run(fn_1, .{});
var F2 = async either.run(fn_2, .{});
either.wait();
if (completed(&F1)) {
// ... handle case of F1
const result = nosuspend await F1;
}
}
test "All" {
var all = All.init();
var F1 = async all.run(fn_1, .{});
var F2 = async all.run(fn_2, .{});
var F3 = async all.run(fn_3, .{});
all.wait();
// use results from F1, F2, or F3 here
} Allowing for checking if an async frame is completed also allows e.g. polling an I/O notifier like epoll/kqueue/IOCP until an async frame is fully completed (where the I/O notifier itself drives the completion of the async frame). pub fn main() !void {
var frame: @Frame(httpRequest) = async httpRequest(....);
while (!completed(&frame)) {
io_notifier.poll();
}
const response = try nosuspend await frame;
} While these operations can be done right now with Zig as is, it requires unnecessary state to be stored which is already maintained in the header of an async frame. |
|
I have another usecase for this functionality. I am using At a high level, in the game loop, rather than storing all the state+data as state variables, and then parsing through them each tick to figure out which state we are in, we use the coroutines as local storage for all the relevant data, while the active coroutine frame would tell us the state. For some more details of using coroutines for state machines, you can see the documentation of the framework: https://randygaul.github.io/cute_framework/#/coroutine/ While a lot of the discussion above is related to multi-threaded issues that a feature like this could cause, in a single threaded case, that should not be an issue. Additionally, the usecases discussed were only for cleanup in case of failure, but here that's not the case. Currently, I am able to use what is available right now, but managing the coroutine stack memory is being done manually. If there was a way to check if the frame is complete, then the coroutines themselves wouldn't need to "mark" themselves as complete, and the memory management would be just checking if the Alternately, if there is some other way to implement coroutines in zig, that would be appreciated as well. |
This feature would also make the proposed Edit: Oops, didn't notice it's already linked. |
There are a few instances where it can be ambiguous whether an async function has completed:
errdefer if (!awaited_download_frame) {
but this can be converted to beerrdefer if (!@frameState(download_frame).awaited) {
It'd be nice if we could check the "state" of the async function. Braindump:
The text was updated successfully, but these errors were encountered: