3

BACKGROUND

After being convinced that C++ stackless coroutines are pretty awesome. I have been implementing coroutines for my codebase, and realised an oddity in final_suspend.

CONTEXT

Let’s say you have the following final_suspend function:

final_awaitable final_suspend() noexcept
{
    return {};
}

And, final_awaitable was implemented as follows:

struct final_awaitable
{
    bool await_ready() const noexcept
    {
        return false;
    }
    default_handle_t await_suspend( promise_handle_t h ) const noexcept
    { 
        return h.promise().continuation();
    }
    void await_resume() const noexcept {}
};

If continuation here was retrieved atomically from task queue and the task queue is potentially empty (which could occur any time between await_ready and await_suspend) then await_suspend must be able to return a blank continuation.

It is my understanding that when await_suspend returns a handle, the returned handle is immediately resumed (5.1 in N4775 draft). So, if there was no avaliable continuation here, any application crashes as resume is called on an invalid coroutine handle after receiving it from await_suspend.

The following is the execution order:

final_suspend                        Constructs final_awaitable.
    final_awaitable::await_ready     Returns false, triggering await_suspend.
    final_awaitable::await_suspend   Returns a continuation (or empty continuation).
        continuation::resume         This could be null if a retrieved from an empty work queue.

No check appears to be specified for a valid handle (as it is if await_suspend returns bool).

QUESTION

  1. How are you suppose to add a worker queue to await_suspend without a lock in this case? Looking for a scalable solution.
  2. Why doesn't the underlying coroutine implementation check for a valid handle.

A contrived example causing the crash is here.

SOLUTION IDEAS

  1. Using a dummy task that is an infinite loop of co_yield. This is sort of wasted cycles and I would prefer not to have to do this, also I would need to create seperate handles to the dummy task for every thread of execution and that just seems silly.

  2. Creating a specialisation of std::coroutine_handle where resume does nothing, returning an instance of that handle. I'd prefer not specialise the standard library. This also doesn't work because coroutine_handle<> doesn't have done() and resume() as virtual.

  3. EDIT 1 16/03/2020 Call continuation() to atomically retrieve a continuation and store the result in the final_awaitable structure, await_ready world return true if there wasn't a continuation available. If there was a continuation available await_ready would return false, await_suspend would then be called and the continuation returned (immediately resuming it). This doesn't work because the value returned by a task is stored in the coroutine frame and if the value is still needed then the coroutine frame must not be destroyed. In this case it is destroyed after await_resume is called on the final_awaitable. This is only an issue if the task is the last in a chain of continuations.

  4. EDIT 2 - 20/03/2020 Ignore the possibility of returning a usable co routine handle from await_suspend. Only resume continuation from top level co routine. This doesn't appear as efficient.

01/04/2020

I still haven't found a solution that doesn't have substantial disadvantages. I suppose the reason I'm caught up on this is because await_suspend appears to be designed to solve this exact problem (being able to return a corountine_handle). I just cannot figure out the pattern that was intended.

David Ledger
  • 2,033
  • 1
  • 12
  • 27
  • Is not it possible to define a `bool await_suspend( promise_handle_t h)` in `final_awaitable` and conditionnaly resume the continuation in the body of this function? – Oliv Mar 14 '20 at 18:53
  • It is possible to return true or false to conditionally resume the coroutine but not a continuation. Still, Its strange that the coroutine handle isn't checked before resuming. It seems to check a flag before resuming any other coroutine with await_ready but doesn't here. Maybe it's just my understanding gap... I just don't see how you are suppose to actually suspend when you have no continuation available and the coroutine isn't ready (and returns coroutine_handle<>). – David Ledger Mar 15 '20 at 00:29

2 Answers2

5

You can use std::noop_coroutine as a blank continuation.

vhavel
  • 146
  • 1
  • 6
1

What about: (Just a large comment in fact.)

struct final_awaitable
{
    bool await_ready() const noexcept
    {
        return false;
    }
    bool await_suspend( promise_handle_t h ) const noexcept
    { 
        auto continuation = h.promise().atomically_pop_a_continuation();
        if (continuation)
           continuation.handle().resume();
        return true;//or whatever is meaningfull for your case.
    }
    void await_resume() const noexcept {}
};
Oliv
  • 17,610
  • 1
  • 29
  • 72
  • Thats a good solution, however the reason that I prefer the coroutine_handle form is that it doesn't grow the stack with each continuation. – David Ledger Mar 15 '20 at 07:14
  • @DavidLedger And if the continuation ends in calling a continuation that ends in calling another continuation... all that coroutine's states will not be destroyed so that the heap could be filled before the stack. I suppose it would be simpler to make a normal function that calls a coroutine, waits for the promise, destroy the coroutine and then calls the continuation. – Oliv Mar 15 '20 at 07:44
  • Yeh, there is usually more heap avaliable than stack, so thats the approach I prefer for scalability. – David Ledger Mar 15 '20 at 07:54