Coroutines, introduced in C++20, bring a new paradigm to asynchronous programming, allowing developers to write code that looks synchronous while operating non-blockingly under the hood. This feature is especially valuable for I/O-bound applications, such as network servers or GUI event loops, where you want to avoid thread contention while maintaining readable code.
What Is a Coroutine?
A coroutine is a function that can suspend its execution at a co_await, co_yield, or co_return point and resume later. Unlike threads, coroutines are lightweight and share the same stack frame, making them far cheaper to create and switch between.
The basic building blocks are:
std::suspend_always and std::suspend_never – traits that dictate when the coroutine should suspend.
std::coroutine_handle – a handle to control the coroutine’s state.
std::future or custom awaitables – objects that provide the await_ready, await_suspend, and await_resume functions.
A Minimal Example
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
struct simple_task {
struct promise_type {
simple_task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
};
simple_task async_print(int x) {
std::cout << "Before suspend: " << x << '\n';
co_await std::suspend_always{}; // Suspend here
std::cout << "After resume: " << x << '\n';
}
Running async_print(42) will pause after printing the first line; resuming the coroutine (via its handle) continues execution.
Integrating with std::async
Although std::async itself is not a coroutine, you can combine them to offload heavy work to background threads while keeping the main flow simple.
std::future <int> compute(int a, int b) {
return std::async(std::launch::async, [=]{
std::this_thread::sleep_for(std::chrono::seconds(2));
return a + b;
});
}
co_await compute(10, 20);
Here the coroutine yields control until the future completes, freeing the calling thread to do other tasks.
Awaitable Types
A type is awaitable if it provides:
await_ready() – returns true if ready immediately.
await_suspend(std::coroutine_handle<>) – called when the coroutine suspends; can schedule resumption.
await_resume() – returns the result when resumed.
A simple example of an awaitable that simulates a timer:
struct timer {
std::chrono::milliseconds delay;
bool await_ready() const noexcept { return delay.count() == 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, delay=delay]{
std::this_thread::sleep_for(delay);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
Using it:
co_await timer{std::chrono::milliseconds(500)};
Practical Use Cases
- Network Servers – Each connection can be handled by a coroutine, suspending on I/O operations without blocking the entire event loop.
- Game Loops – Coroutine-based animation sequences allow for clean sequencing of actions over frames.
- GUI Frameworks – UI callbacks can be coroutine-friendly, enabling asynchronous file loading or background computations.
Challenges and Tips
- Error Propagation: If an exception is thrown inside a coroutine, the promise’s
unhandled_exception() is called. Ensure proper exception handling or propagate via std::exception_ptr.
- Lifetime Management: The coroutine must outlive any references it captures. Prefer move semantics or store data on the heap.
- Debugging: Coroutines can be harder to trace. Using tools like
std::coroutine_handle::address() can help identify specific coroutine instances.
Conclusion
C++20 coroutines open a door to elegant, efficient asynchronous programming. By embracing co_await and custom awaitables, developers can write code that feels imperative while leveraging non-blocking execution patterns. Whether building high-performance servers or responsive UI applications, coroutines provide a powerful addition to the modern C++ toolkit.