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_alwaysandstd::suspend_never– traits that dictate when the coroutine should suspend.std::coroutine_handle– a handle to control the coroutine’s state.std::futureor custom awaitables – objects that provide theawait_ready,await_suspend, andawait_resumefunctions.
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 viastd::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.