Awaitables form the basis of C++20’s implementation of coroutines, and for good reason: they provide a simple, flexible, and generic interface that allows you to build pretty much any kind of coroutine. What’s more, awaitables are the way to make existing classes and types usable with the powerful features that coroutines provide.
Parts of an Awaitable
All awaitables have three parts.
A way to check if we need to suspend the coroutine.
If the operation or value is already ready, we don’t need to suspend. Awaitables have a function, await_ready()
, that does that check and returns either true or false. It’s perfectly fine to always return true, or always return false, depending on the task.
The thing to do once we suspend.
This could start an asynchronous operation, for example, or sumbit a task to a thread pool, or do an OS call. All this behavior goes in await_suspend()
. You can even start up a different coroutine, and if await_suspend()
returns a coroutine handle, the program will jump to that coroutine as soon as the one calling co_await
suspends. This allows you to chain unlimited numbers of coroutines together without there being a stack overflow!
The function await_suspend
takes one argument, a coroutine handle, and we can use this as a callback to resume the awaiting corouting as soon as our operation is finished.
The thing to do when we resume.
Oftentimes, you might want to do something when your coroutine resumes. This is what await_resume()
controls. You can also use it to get a value from the co_await
expression! If my_awaitable.await_resume()
returns a value, then co_await my_awaitable
will also return that value!
So what happens when we call co_await?
When we call co_await w;
, the program checks w.await_ready()
. If this is false, then we call await_suspend()
with the handle of the current coroutine, and the coroutine suspends. Otherwise, we go straight to await_resume()
and don’t suspend. If the coroutine was suspended, then await_resume()
will be called once it resumes. Either way, await_resume()
is the last thing that’s called as part of co_await
.
The code that does this looks something like this, with compiler intrinsics in all-caps:
if (!w.await_ready()) {
w.await_suspend(__GET_CURRENT_HANDLE());
__SUSPEND_COROUTINE();
}
// This only occurs once resume() is called on the handle for this coroutine
w.await_resume();
Note on optimizations
If the definition for await_ready
, await_suspend
, and await_resume
are availible to the compiler, it can inline them and get rid of uncessesary checks, just like it would any other function! For example, with the std::suspend_always
type, await_ready()
will always return false and both await_suspend
and await_resume()
do nothing, so the compiler won’t have to do a check and code co_await std::suspend_always{}
can be simplified from:
// Before simplification
auto&& w = std::suspend_always{};
if (!w.await_ready()) { // This if is always true
w.await_suspend(__GET_CURRENT_HANDLE()); // This does nothing
__SUSPEND_COROUTINE(); // We still have to suspend
}
w.await_resume(); // This also does nothing
To:
// After simplification
__SUSPEND_COROUTINE(); // We still have to suspend
Creating a Concept that represents an awaitable
We can create a concept for an awaitable that captures the interface of what an awaitable must provide. For an awaitable,
await_ready()
must return abool
;await_suspend()
must return eithervoid
,bool
, orstd::coroutine_handle<>
await_resume()
has to exist, but it can return anything.
We can translate this pretty directly into the code for a concept:
// Checks that T is one of a list of types
template <class T, class... Types>
concept is_either = (std::same_as<T, Types> || ...);
template <class T>
concept awaitable = requires(T t, std::coroutine_handle<> handle) {
{ t.await_ready() } -> std::same_as<bool>;
{ t.await_suspend(handle) } -> is_either<void, bool, std::coroutine_handle<>>;
{ t.await_resume() };
};
We can use this concept to check if a type is awaitable, and also to write constrained function templates, like this:
coroutine my_coroutine(awaitable auto w) {
std::cout << "my_coroutine: Began coroutine\n";
co_await w;
std::cout << "my_coroutine: Exiting coroutine\n";
}
Here, we have a simple coroutine that does three things:
- It prints the begin message
- It awaits on an input,
w
, which we require to be awaitable - It prints the exiting message
We can run the coroutine until it’s done:
int main() {
coroutine co = my_coroutine(std::suspend_always{});
while (!co.done()) {
std::cout << "Resuming coroutine...\n ";
co.resume();
}
std::cout << "Done.\n";
}
You can test this code here! It will output the following text:
Resuming coroutine...
my_coroutine: Began coroutine
Resuming coroutine...
my_coroutine: Exiting coroutine
Done.
A Few Common Awaitables
Now that we’ve covered what an awaitable is and what it’s for, I’m going to provide a few different awaitables that do various things. We’ll start with std::suspend_always
and std::suspend_never
, and progress from there.
suspend_always and suspend_never
The simplest possible awaitables are suspend_always
and suspend_never
, and they can be implemented as follows. For suspend_always
, this is:
struct suspend_always {
// Because we always suspend, this always returns false
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
void await_resume() {}
};
The implementation for suspend_never
is nearly identical, although await_ready()
will always return true, indicating that the coroutine won’t suspend:
struct suspend_never {
// Because we never suspend, this always returns true
bool await_ready() { return true; }
void await_suspend(std::coroutine_handle<>) {}
void await_resume() {}
};
In a library implementation, all of these functions can be marked as constexpr
so that the compiler can optimize out checks for await_ready
, although I’ve ommitted this for simplicity.
Awaitable with Callback
Earlier, I mentioned that if await_suspend
returns a coroutine handle, then the program will resume that handle once the calling coroutine suspends. We can use this to implement an awaitable type that will resume a different coroutine when you suspend it, and this can be done in only a few lines of code:
struct awaitable_callback {
std::coroutine_handle<> callback = std::noop_coroutine();
constexpr bool await_ready() noexcept {
return false;
}
auto await_suspend(std::coroutine_handle<>) noexcept {
return callback;
}
void await_resume() noexcept {}
};
When you call co_await
on an awaitable_callback
, the program will immediately call callback.resume()
. If callback
were null, the program would crash, so instead we set callback
to std::noop_coroutine()
by default because if no callback is set, then nothing should happen.
I used awaitable_callback
in the implementation of the next type: an awaitable_task
.
Awaitable Task
The conduit library that I’ve written as a companion to these articles provides a awaitable_task
type, which is a coroutine you can await on. Here, set_callback
will store the callback inside the promise type, and then we’ll resume the task by returning the handle to the task from await_suspend
. Once the task is ready, it’ll invoke the callback and await_resume()
will be called to get the value.
Something cool to note is that this is the first time we have an await_ready()
that doesn’t just return true or false! Here, if the task is already done, we don’t need to suspend so await_ready()
will return true. Otherwise, the task isn’t finished yet, so await_ready()
will return false.
template<class Promise>
struct awaitable_task : unique_handle<Promise> {
using base_type = unique_handle<Promise>;
using base_type::base_type;
bool await_ready() noexcept {
return base_type::done();
}
auto await_suspend(std::coroutine_handle<> callback) noexcept {
base_type::promise().set_callback(callback);
return base_type::get_raw_handle();
}
auto await_resume() {
return base_type::promise().get_value();
}
};
Here’s the implementation of the default promise type for awaitable_task
. In order to give control to the callback, we return an awaitable_callback
from final_suspend
.
template <class ReturnValue>
struct task_promise : promise_base<task_promise<ReturnValue>, awaitable_task, true, false> {
std::coroutine_handle<> callback = std::noop_coroutine();
ReturnValue returned_value = {};
auto final_suspend() noexcept {
return awaitable_callback{callback};
}
void set_callback(std::coroutine_handle<> callback) noexcept {
this->callback = callback;
}
void return_value(ReturnValue value) {
returned_value = std::move(value);
}
ReturnValue get_value() { return std::move(returned_value); }
};
Together, these two things make it trivial to define an awaitable future type:
template <class T>
using future = awaitable_task<task_promise<T>>;
Bonus: Logging Awaitable
We can write an awaitable that prints a message every time one of it’s functions are called, allowing us to observe the behavior of co_await
. It’s not very useful, but it’s a good illustration of what’s going on under the hood!
struct logging_awaitable {
bool is_ready = false;
bool await_ready() {
std::cout << "await_ready()\n";
return is_ready;
}
void await_suspend(std::coroutine_handle<> h) {
std::cout << "await_suspend(" << h.address() << ")\n";
}
void await_resume() {
std::cout << "await_resume()\n";
}
};
Conclusion
Alright! That wraps up this article for now! If you have any questions, feel free to reach out to me!