Coroutines in C++
Coroutines at its core are functions that can be paused and resumed, enabling asynchronous and lazy computations in a natural, sequential style.
Coroutines are special functions that can suspend their execution at certain points and resume later from where they left off. Unlike regular functions that run to completion once called, coroutines allow for interleaved execution, making it possible to implement various asynchronous and stateful behaviors in a more structured and readable way.
Coroutines make it possible to write code like:
co_await some_async_op();
Instead of callback chains or manually managing state machines.
Unlike regular functions coroutines :
Simple example of coroutine :
#include <coroutine>
#include <iostream>
// A minimal coroutine that prints messages
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task myCoroutine() {
std::cout << "Coroutine started\n";
co_await std::suspend_always{}; // Suspend here
std::cout << "Coroutine resumed\n";
}
int main() {
myCoroutine(); // Output: "Coroutine started"
}
Coroutines are not a standalone feature but a framework. The compiler needs to know:
This is done by defining:
Key Components
Promise Type
Every coroutine has an associated promise_type defining its behavior:
Methods:
Coroutine Handle
std::coroutine_handle<Promise> is a low-level interface to interact with the coroutine:
Coroutine Lifetime & States
Scheduling & Resumption
Coroutines do not have built-in scheduling—the programmer defines how/when they resume. This is controlled by awaiters (objects returned by co_await).
Memory Considerations
Step-by-Step implementation using a Coroutine
Let's create step-by-step a simple generator coroutine that yields numbers.
Step 1: Include Coroutine Headers
#include <coroutine>
#include <iostream>
Step 2: Define the Return Type
Coroutines don’t return like normal functions. We need to define a custom type that wraps the coroutine.
struct Generator {
handle_type coro;
explicit Generator(handle_type h) : coro(h) {}
~Generator() {
if (coro) coro.destroy();
}
bool next() {
coro.resume();
return !coro.done();
}
int value() const {
return coro.promise().current_value;
}
// Below is defining the promise_type
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
int current_value;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
};
Step 3 :Write the Coroutine Function
Generator counter(int max) {
for (int i = 0; i <= max; ++i) {
co_yield i;
}
}
Step 4: Use It!
int main() {
auto gen = counter(5);
while (gen.next()) {
std::cout << "Value: " << gen.value() << "\n";
}
}
Details of promise_type
Underhood of coroutine operation :
When you write a coroutine function like:
Generator counter(int max) {
for (int i = 0; i <= max; ++i) {
co_yield i;
}
}
The compiler rewrites this function into a state machine and generates calls to special hooks. These hooks are defined in a struct named promise_type as described earlier .
What Is promise_type?
Breakdown of the promise_type Members
struct promise_type {
int current_value; // Stores the value to be yielded
1. Generator get_return_object()
Recommended by LinkedIn
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
using handle_type = std::coroutine_handle<promise_type>;
2. std::suspend_always initial_suspend()
std::suspend_always initial_suspend() { return {}; }
3. std::suspend_always final_suspend() noexcept
std::suspend_always final_suspend() noexcept { return {}; }
4. std::suspend_always yield_value(int value)
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
5. void return_void()
void return_void() {}
void return_value(T value);
6. void unhandled_exception()
void unhandled_exception() { std::exit(1); }
void unhandled_exception() {
std::rethrow_exception(std::current_exception());
}
handle_type coro;
handle_type::from_promise(*this)
We use coro.resume(), coro.destroy(), and coro.done() to control it from the outer class (Generator).
Coroutines vs. Threads:
Coroutines are not a direct replacement for threads—they serve different purposes:
Threads are about parallelism, managed by the OS, and can run concurrently on multiple CPU cores. Coroutines are about concurrency, managed at the application level, and are cooperative. So coroutines aren't a replacement but a complement.
Coroutines are ideal for:
Threads are still needed for:
Simple example where multiple coroutines run concurrently without sharing resources.
Each coroutine performs an independent task (printing numbers with a delay) to demonstrate parallel execution without race conditions:
#include <coroutine>
#include <iostream>
#include <thread>
#include <vector>
// Coroutine return type
struct AsyncTask {
struct promise_type {
AsyncTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
// Awaiter for non-blocking delay
struct DelayAwaiter {
int ms;
bool await_ready() { return false; } // Always suspend
void await_suspend(std::coroutine_handle<> h) {
// Resume after delay (non-blocking)
std::thread([h, this] {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
h.resume();
}).detach();
}
void await_resume() {}
};
// Coroutine task: Print numbers with a delay
AsyncTask printNumbers(int id, int count, int delay_ms) {
for (int i = 1; i <= count; ++i) {
std::cout << "Coroutine " << id << ": " << i << std::endl;
co_await DelayAwaiter{delay_ms}; // Suspend with delay
}
}
int main() {
// Launch 3 independent coroutines
printNumbers(1, 3, 100); // Prints 1, 2, 3 with 100ms delay
printNumbers(2, 3, 200); // Prints 1, 2, 3 with 200ms delay
printNumbers(3, 3, 150); // Prints 1, 2, 3 with 150ms delay
// Wait for coroutines to finish
std::this_thread::sleep_for(std::chrono::seconds(1));
}
/*
Coroutine 1: 1
Coroutine 2: 1
Coroutine 3: 1
Coroutine 1: 2
Coroutine 3: 2
Coroutine 2: 2
Coroutine 1: 3
Coroutine 3: 3
Coroutine 2: 3
*/
Things to notice :
Example for Multiple Coroutines with Shared Resource
Let’s implement a thread-safe counter accessed by multiple coroutines running on a thread pool. We’ll use:
#include <mutex>
#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <coroutine>
#include <future>
//Step 1: Shared Resource with Mutex
struct SharedCounter {
std::mutex mtx;
int value = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++value;
}
int get() {
std::lock_guard<std::mutex> lock(mtx);
return value;
}
};
//Step 2: Thread Pool & Async Task
class ThreadPool {
std::vector<std::jthread> workers;
std::queue<std::function<void()>> tasks;
std::mutex q_mutex;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t threads) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(q_mutex);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<typename F>
void enqueue(F&& f) {
{
std::lock_guard lock(q_mutex);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
std::lock_guard lock(q_mutex);
stop = true;
}
cv.notify_all();
}
};
// Global thread pool
ThreadPool pool(4);
struct AsyncTask {
struct promise_type {
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
AsyncTask get_return_object() { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
struct ResumeOnPool {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
pool.enqueue([h] { h.resume(); });
}
void await_resume() {}
};
AsyncTask incrementCounter(SharedCounter& counter) {
co_await ResumeOnPool{}; // Resume on thread pool
counter.increment();
}
//Step 3: Run Coroutines in Parallel
int main() {
SharedCounter counter;
// Launch 1000 coroutines
for (int i = 0; i < 1000; ++i) {
incrementCounter(counter);
}
// Wait for all tasks to complete
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Final counter value: " << counter.get() << "\n";
}
Key Points
We often see "return {}" , what is it ?
"return {}" is a shorthand for returning a default-initialized value of the return type of the function.
Ex:
std::suspend_always final_suspend() noexcept {
return {};
}
It means "return a default-constructed std::suspend_always".
In C++, {} is value-initialization syntax (aka brace initialization), which:
Complete Generator Example :
#include <iostream>
#include <coroutine>
template <typename T>
struct generator {
struct promise_type {
T current_value;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
generator get_return_object() { return generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle<promise_type>;
generator(handle_type h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
generator(generator&& other) noexcept : coro(other.coro) {
other.coro = nullptr;
}
generator& operator=(generator&& other) noexcept {
if (this != &other) {
if (coro) coro.destroy();
coro = other.coro;
other.coro = nullptr;
}
return *this;
}
bool next() {
coro.resume();
return !coro.done();
}
T value() const { return coro.promise().current_value; }
private:
handle_type coro;
};
generator<int> fibonacci_sequence(int n) {
if (n <= 0) {
co_return;
}
int a = 0;
int b = 1;
if (n >= 1) co_yield a;
if (n >= 2) co_yield b;
for (int i = 3; i <= n; ++i) {
int next = a + b;
co_yield next;
a = b;
b = next;
}
}
generator<int> getNext(int start = 0, int step = 1) noexcept {
auto value = start;
for (int i = 0;; ++i){
co_yield value;
value += step;
}
}
int main() {
std::cout << "First 10 Fibonacci numbers:" << std::endl;
for (auto gen = fibonacci_sequence(10); gen.next(); ) {
std::cout << gen.value() << " ";
}
std::cout << std::endl;
std::cout << "getNext():";
auto gen = getNext();
for (int i = 0; i <= 10; ++i) {
gen.next();
std::cout << " " << gen.value(); // (7)
}
std::cout << "\n\n";
std::cout << "getNext(100, -10):";
auto gen2 = getNext(100, -10);
for (int i = 0; i <= 20; ++i) {
gen2.next();
std::cout << " " << gen2.value();
}
return 0;
}
/*
First 10 Fibonacci numbers:
0 1 1 2 3 5 8 13 21 34
getNext(): 0 1 2 3 4 5 6 7 8 9 10
getNext(100, -10): 100 90 80 70 60 50 40 30 20 10 0 -10 -20 -30 -40 -50 -60 -70 -80 -90 -100
*/
Good read: Coroutines – MC++ BLOG