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
- How are you suppose to add a worker queue to await_suspend without a lock in this case? Looking for a scalable solution.
- Why doesn't the underlying coroutine implementation check for a valid handle.
A contrived example causing the crash is here.
SOLUTION IDEAS
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.
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.
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.
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.