Skip to content
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

Open
fengb opened this issue Sep 3, 2019 · 6 comments
Open

Proposal: a way to check async frame liveness #3164

fengb opened this issue Sep 3, 2019 · 6 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@fengb
Copy link
Contributor

fengb commented Sep 3, 2019

There are a few instances where it can be ambiguous whether an async function has completed:

  1. Error handling — the example uses errdefer if (!awaited_download_frame) { but this can be converted to be errdefer if (!@frameState(download_frame).awaited) {
  2. Generators — liveness is an indicator of completion, but there's currently not a way for the execution context to check

It'd be nice if we could check the "state" of the async function. Braindump:

const FrameState = struct {
    running: bool, // not sure if we should expose "suspended" as it sounds like a concurrent problem
    awaited: bool,
};

@frameState(f: anyframe) FrameState
@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Sep 3, 2019
@andrewrk andrewrk added this to the 0.6.0 milestone Sep 3, 2019
@andrewrk
Copy link
Member

andrewrk commented Sep 3, 2019

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 @Frame(func) instance):

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));
}
    running: bool, // not sure if we should expose "suspended" as it sounds like a concurrent problem

Indeed, this is problematic. Even the awaited field wouldn't be exposed directly, as it would require an atomic operation. Implementing this would mean adding a caller-managed flag to the frame. Just a convenience so the caller doesn't have to introduce their own flag to manage the state.

I'll create a sister proposal for cancel. I haven't given up hope on that yet. If that panned out I think this proposal would be unneeded. The goal of the cancel proposal would be:

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).

@tecanec
Copy link
Contributor

tecanec commented Nov 2, 2020

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.
As a use case for this proposal, let's say that your game engine is loading a file. You want to do something to that file if it's already loaded, but if it isn't, you'd rather try again later than risk wasting valuable milliseconds by waiting like you would by using await directly. There are ways to do this currently, but none of them are elegant enough that I wouldn't consider a better solution worthwhile.

@lithdew
Copy link
Contributor

lithdew commented Feb 26, 2021

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.

@daurnimator
Copy link
Contributor

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'.

async/await are "low level" syntax; I think you're looking for std.event.Batch

@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 May 19, 2021
@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 23, 2021
@samhattangady
Copy link
Contributor

samhattangady commented Dec 17, 2021

I have another usecase for this functionality.

I am using suspend and resume to implement a coroutines system. This is inspired by Randy Gaul's talk at Handmade Seattle 2021, where coroutines are used to simplify the creation and management of a state machine.

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 frame has been completed, in which case the memory can be deinitted.

Alternately, if there is some other way to implement coroutines in zig, that would be appreciated as well.

@thedmm
Copy link
Contributor

thedmm commented Mar 3, 2022

This feature would also make the proposed select syntax trivial.

Edit: Oops, didn't notice it's already linked.

@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jul 9, 2023
@andrewrk andrewrk modified the milestones: 0.14.0, 0.15.0 Feb 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

7 participants