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: Typed suspend #7291

Closed
ghost opened this issue Dec 3, 2020 · 7 comments
Closed

Proposal: Typed suspend #7291

ghost opened this issue Dec 3, 2020 · 7 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@ghost
Copy link

ghost commented Dec 3, 2020

Inspired by discussion on #5913. Conceived first by @kprotty, although he never wrote it down 😉

Due to technical issues (pointed out by King) involving nested frames and the stubborn nature of some functions, cancelling a function from the awaiter side is sadly impractical. Something resembling cancellation is possible from the resumer side, however I believe that implementing this at the language level is a folly -- our job as language designers is not to provide every feature, but to provide only the features necessary to build whatever abstractions are suitable.

To that end, I propose one change to suspension semantics: rather than being valueless, suspend and suspend blocks will return a value to the function in which they appear, passed to them by the corresponding resume. An event loop may use this to inform a function how to proceed:

// In the suspending function
const action = suspend event_loop.registerContinuationAndCancellation(
    @frame(),
    continuation_condition,
    cancellation_condition,
);

switch (action) {
    .go => {},
    .stop => return error.functionXCancelled;
}

// ------------------

// In the event loop (some details missing)
if (frame.continuation and @atomicRmw(bool, &frame.suspended, .Xchg, false, .Weak) {
    const ptr = frame.ptr;
    frame = null;
    resume ptr, .go;
}

if (frame.cancellation and @atomicRmw(bool, &frame.suspended, .Xchg, false, .Weak) {
    const ptr = frame.ptr;
    frame = null;
    resume ptr, .stop;
}

The invoker may specify a cancellation condition as argument (e.g. a timeout for a web request), and then the function will register the appropriate callbacks with the event loop. The event loop may even provide a method to generate an awaiter-triggerable cancel token, one end of which could be passed to a generically cancelable function. If a function is not cancelable (some file operations on some kernels), it can simply not take such a condition, and the user will not mistakenly try to cancel it.

Since @frame() may be called anywhere within the function, and the resumer needs to know the type before analysing the frame, the suspend type (T in anyframe<-T) must be part of the function's signature. I propose we reuse while loop continuation syntax:

const suspendingFunction = fn (arg: Arg) ReturnType : SuspendType {
    // ...
};

Any function that uses the suspend keyword must have a suspend type. This is not function colouring, as any function with explicit suspend is necessarily asynchronous anyway (functions that only await cannot be keyword-resumed, so do not need a suspend type). The suspend type may be void or error!void (no error set inference, since such errors originate outside the function), in which case the handle type is anyframe<-void or anyframe<-error!void (not anyframe -- we require strongly typed handles for type checking, which is one drawback), and resume does not necessarily take a second argument, as in status quo.

This not only permits flexible evented userspace cancellation, but also more specialised continuation conditions: a function waiting for multiple files to become available could receive a handle to the first one that does, and combined with a mechanism to check whether a frame has completed, #5263 could be implemented in userspace in the same manner.

At first blush, this may appear to be hostile to inlining async functions -- however, allowing that would already require semantic changes (#5277) that actually complement this quite nicely: @frame() would return anyframe<-T of the syntactically enclosing function's suspend type, regardless of the suspend type of the underlying frame, and there is now a strict delineation between resumable and awaitable handles.

@Vexu Vexu added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Dec 3, 2020
@Vexu Vexu added this to the 0.8.0 milestone Dec 3, 2020
@kprotty
Copy link
Member

kprotty commented Dec 3, 2020

Small nitpick, but the check for &frame.suspended before resumeing it can be merged into the resume itself.

@ghost
Copy link
Author

ghost commented Dec 3, 2020

That's meant to be an atomic lock -- so if the continuation and cancellation resolve at the same time, they won't both try to resume at once. Double resume is UB in unsafe modes (or should be, if I understand correctly). I did notice some obvious holes in that impl though, so I've patched them (there might be more, but it's only for demonstration anyway, so who cares).

@rohlem
Copy link
Contributor

rohlem commented Dec 4, 2020

I think this can already (somewhat) be implemented in status-quo by communication via pointers. Untested:

const action = enum{.go, .stop};
fn asynchronous_task(action_ptr_loc: **action) result_type {
  var resumer_requested_action: action = .go;
  action_ptr_loc.* = &requested_action;
  // ...
  suspend;
  switch(resumer_requested_action){
    // decide what to do...
  }
  // ...
}
fn caller(){
  var request_ptr: *action = undefined;
  var aframe = async asynchronous_task(&request_ptr);
  // ...
  if() { //the same atomic-exchange you did
    request_ptr.* = if(should_stop) .stop else .go;
    resume aframe;
  }
}

The downside is that you have to transport both aframe and request_ptr to a controlling resumer, which this proposal would solve.

I think the ability to upcast from anyframe<-T to anyframe<-void to allow unadorned resume aframe; (keeping the last suspend value unchanged) might also be useful.
Maybe not having that option (-> not requiring it in the async callee) is the saner default, but the only userspace workaround I can come up with requires an intermediate function as a seam and is really verbose.

there is now a strict delineation between resumable and awaitable handles

It can still be useful to have a type-erased handle for both, until you decide to split it up at a later point in the code - I assume that would be anyframe<-S->T .

Also, small nitpick, the syntax resume a, b; is a bit ambiguous if you consider that anyframe is itself a valid suspend-resume-parameter type.
Maybe resume(action) frame; would be clearer, f.e. resume(.stop) aframe;

@kyle-github
Copy link

De-lurking for a second.

Zig already has extended the meaning of else in other ways. What about something like this:

const result = suspend {
    event_loop.registerContinuation(
        @frame(),
        ...
    );
} else {
    // do cancellation actions.
}

This may be the wrong place to put that, but my understanding of the whole idea is that there are really only two things that can happen to a continuation from outside: resume and cancel. And if cancel can only be caught/happen at suspend points, then it seems like else might be sufficient.

Just a random thought...

Re-lurking.

@ghost
Copy link
Author

ghost commented Dec 4, 2020

I think the ability to upcast from anyframe<-T to anyframe<-void to allow unadorned resume aframe; (keeping the last suspend value unchanged) might also be useful.

One problem: where do you store that value?

It can still be useful to have a type-erased handle for both, until you decide to split it up at a later point in the code

As per discussion on #5277, no, this is not generally useful, and should not be encouraged. There is @ptrCast if such behaviour is desired.

the syntax resume a, b; is a bit ambiguous

The first argument is the frame, the second is the suspend value. This is the case at every resume invocation. There is no ambiguity.

there are really only two things that can happen to a continuation from outside: resume and cancel

There are other use cases for suspend values as well -- read the proposal.

Zig already has extended the meaning of else in other ways

else means the same thing everywhere -- "if this condition is not met, and we're still evaluating, run this instead". This does not apply here -- the suspend body is always run, and whether the else is or not depends on outside influence.

@rohlem
Copy link
Contributor

rohlem commented Dec 6, 2020

allow unadorned resume aframe; (keeping the last suspend value unchanged)

One problem: where do you store that value?

Inside the async function's stack frame, like it is in the status-quo code I posted.
The resume-parameter will already end up on the function's stack.
This change would require an initial default value (or potentially passed at every suspend point), and extend the lifetime of that memory slot to not be disjoint at every suspend point, but be allocated at the first and stay alive up until the last one.

The first argument is the frame, the second is the suspend value. [...] There is no ambiguity.

I meant ambiguity from a reader's perspective who's never encountered that syntax before, potentially mistaking it for a multi-resume (compare resume a; to resume a, b;), not ambiguity of the proposed ruleset.

@ghost
Copy link
Author

ghost commented Dec 27, 2020

Inside the async function's stack frame, like it is in the status-quo code I posted.

No good -- the resumer can't know whether such a value is stored, and allocating space for it on the off-chance someone might want to do that is just wasteful.

Upon further consideration, this is a bad idea. #5277 already covers differentiating anyframe types, and this can be accomplished in status quo by passing a pointer to local data when registering with the event loop. This is just unnecessary complexity.

@ghost ghost closed this as completed Dec 27, 2020
This issue was closed.
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

4 participants