Some theory on how C++ coroutines work.
Coroutine is a function that can pause execution in several suspension points.
Function is considered a coroutine if contains one of keywords co_await, co_return or co_yield.
co_awaitsuspends coroutine while some async operation completes, and passes it as completion handler to that async operation.co_returnsets return value of coroutine and completes execution of coroutine body (but coroutine promise may have additinal actions to do).co_yieldpasses one value to yield handler andco_await's while it is processed.
Here is some coroutine:
ReturnType test_coro(int param) {
// Just return param
co_return param;
}We see co_return in function body, so it is coroutine.
Compiler rewrites coroutine body as:
ReturnType test_coro(int param) {
// Allocated on real stack frame:
ReturnType return_object;
{
"Create coroutine frame and jump into it";
"Copy args to coroutine frame";
ReturnType::promise_type promise;
// Initialize return object on stack frame of caller:
return_object = p.get_return_object();
try {
co_await promise.initial_suspend(); // suspension point 1
// Here is original test_coro() body:
// Just return param
co_return param;
catch (...) {
promise.unhandled_exception();
}
co_await promise.final_suspend(); // suspension point 2
"RAII destruction of promise, args and coroutine frame";
}
}Note that there are coroutine keywords, and they are rewritten too in next passes.
But let's read from start. First, coroutine function is returning some value.
It is "return object" - external interface to a running coroutine. It may do nothing but it SHOULD contain one type alias - ReturnType::promise_type.
Promise is an implementation of all calls to promise class in generated code above.
C++ devs may someway customize it for their own needs.
It has the following interface:
struct Promise {
ReturnType get_return_object();
Awaitable initial_suspend();
void return_value(T); // or void return_void();
Awaitable final_suspend();
void unhandled_exception();
};get_return_object is function for receiving return object from promise.
initial_suspend is awaitable object that is usually one of two types - std::suspend_always or std::suspend_never.
If coroutine handle should be suspended prior to execution, then std::suspend_always awaitable is returned.
It is used if caller should do some actions on called coroutines before continue.
Then coroutine body is executed, and execution is finished with co_return. co_return calls promise.return_value() and exits coroutine body scope.
If something gets wrong promise.unhandled_exception is called. Use std::current_exception() to receive exception.
And finally final_suspend is Awaitable object that may pass control to some completion handler, which receive returned value or exception, if it was not happened before.
This handler is usually another coroutine that awaits current one. Or something synchronously waiting coroutine to complete.
Okay, but one thing is unclear yet - what is Awaitable.
Awaitable is something that passed to co_await.
It has the following interface:
struct Awaitable {
bool await_ready();
auto await_suspend(coroutine_handle<> caller);
Ret await_resume();
};It is usually a handle of some "paused" or "completed" async operation.
First co_await asks if operation is paused or completed using await_ready(). True means "complete", false is "paused".
If it is paused, current coroutine should be suspended.
co_await implementation suspends current coroutine and calls await_suspend(caller).
After completion of awaitable operation, or if async operation was already completed, await_resume() is called. Value returned from await_resume() is result of whole co_await expression.
In example above, expression co_await some_expression() is rewritten as:
auto temporary = some_expression();
auto awaitable = temporary.operator co_await();
if (!awaitable.await_ready()) {
if (coroutine.suspend() == SUSPENDED) {
"Jump from coroutine frame to thread stack";
// Pass suspended coroutine to awaitable:
auto ret = awaitable.await_suspend(coroutine);
// Optional: If await_suspend returns some another coroutine, resume it:
if (ret) { ret.resume(); }
// Just return from current function call in terms of thread stack
// It may be "ReturnType test_coro(int)" and "void `ReturnType test_coro(int)`::.resume()":
return;
}
// Point of resumption
}
auto result = awaitable.await_resume()
// And result of the whole expression is:
resultThere are two standard Awaitables - std::suspend_always and std::suspend_never.
std::suspend_always tells co_await to just suspend execution. Note that caller handle is not saved anywhere, so it is very special.
std::suspend_never tells co_await to do nothing.
So, if we implement ReturnType and it's promise_type, then we can write coroutines.
Inside coroutine, we may use co_await of some asynchronous operations. Such operations should return Awaitable interfaces.
But how to make coroutine Awaitable itself?
There is simple recipe:
- Coroutine promise should return
std::suspend_alwaysininitial_suspend. - Coroutine return object should contain
operator co_await()that returnsAwaitablethat is alwaysawait_ready() == false. - Then caller coroutine will be suspended and passed into
await_suspendofAwaitablereturned on previous stage. Inside ofawait_suspendwe may save the caller to Promise and resume the initial-suspended callee. - In
final_suspendof callee's Promise we may return control to saved caller. - Caller then calls
await_resume()to receiveco_returned result. After that,Awaitablecallscoroutine.destroy()to clean up memory of callee.
Here is simplified implementation of our task class:
template<typename Ret>
struct Task {
struct Promise;
using promise_type = Promise; //< NOLINT: coroutine trait
std::coroutine_handle<Promise> handle;
auto operator co_await() const & noexcept {
struct Awaitable {
std::coroutine_handle<Promise> handle;
bool await_ready() const noexcept {
// We need to save caller
return false;
};
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) const noexcept {
// Save caller
handle.promise().caller = h;
return handle;
}
Ret await_resume() noexcept {
Ret ret = std::move(handle.promise().ret.value());
// Destroy handle because it is in final_suspended state if we get here
handle.destroy();
return ret;
}
};
return Awaitable{.handle = handle};
}
struct Promise {
Promise() = default;
std::coroutine_handle<> caller{};
std::optional<Ret> ret;
std::suspend_always initial_suspend() noexcept { return {}; }
void return_value(Ret &&result) {
ret = std::move(result);
}
void return_value(const Ret &result) {
ret = result;
}
auto final_suspend() noexcept {
struct Awaitable {
bool has_caller;
bool await_ready() noexcept { return !has_caller; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> h) noexcept {
// Pass control to the caller without creating additional stack frame.
// h will be freed in `operator co_await()::Awaitable::await_resume`
return h.promise().caller;
};
void await_resume() noexcept {
}
};
return Awaitable{.has_caller = (caller.address() != nullptr)};
}
void unhandled_exception() noexcept {
*(int *) 0x142 = 42;
}
Task get_return_object() {
return {.handle = std::coroutine_handle<Promise>::from_promise(*this)};
}
};Let's see the following code:
1: Task<int> coro2() {
2: co_return 42;
3: }
...
10: // Inside another calling coroutine...
11: int x = co_await coro2();-
Line 10.
- Starting to evaluate
co_await coro2(). - First,
coro2()is called. Go to line 1.
- Starting to evaluate
-
Line 1:
- Coroutine frame is created
Task::Promiseis created on coroutine stack.Promise::initial_suspend()returnsstd::suspend_always. So,coro2()as coroutine is suspended.coro2()as function returnsPromise::get_return_object(), where Task object is created.- Back to line 10.
-
Line 10:
-
co_awaitof returned object(Task) is called. Compiler checks if returned object isAwaitable. Since it is not,Task::operator co_await()is applied. -
It returns
Awaitable, which returnsAwaitable::await_ready()=false. -
The last means that current coroutine should be suspended and passed to
Awaitable::await_suspend(coroutine_handle). -
Inside that function, caller is saved inside
Task::Promiseofcoro2(). Thencoro2()is resumed. Go to line 2.
-
-
Line 2:
- We see
co_return 42. It means thatPromise::return_value(42)is called. It stores return value. - Then
Promise::final_suspend()is called. It transfers execution to caller, saved on previous line. Back to line 10.
- We see
-
Line 10:
Awaitable::await_resume()is called, and it is result of wholeco_await. Insideawait_resume,final_suspended coroutine is destroyed, and saved value returned to caller.
Expression is finally evaluated.