Softwareadvancedcpp20coroutinesasyncconcurrencysystemcparallel
C++ Coroutines (C++20)
Comprehensive guide to C++20 coroutines: syntax, implementation patterns, and SystemC integration.
30 min read
Updated 9/8/2024
1 prerequisite
Prerequisites
Make sure you're familiar with these concepts before diving in:
C++ Fundamentals and Performance
Learning Objectives
By the end of this topic, you will be able to:
Understand C++20 coroutine fundamentals and promise types
Implement custom awaitable types and executors
Master coroutine lifecycle and memory management
Apply coroutines in SystemC and concurrent systems
Table of Contents
C++ Coroutines (C++20)
C++20 coroutines provide language-level support for suspendable functions, enabling elegant asynchronous and concurrent programming patterns.
1. Core Concepts
1.1 Coroutine Keywords
co_await
: Suspend execution until awaitable is readyco_yield
: Suspend and produce a value (generators)co_return
: Return from coroutine and complete execution
1.2 Promise Type Contract
Every coroutine type must define a promise_type
with specific interface:
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object(); // Creates return object
std::suspend_never initial_suspend(); // Start immediately or suspended?
std::suspend_always final_suspend(); // Suspend at end?
void return_void(); // Handle co_return;
void unhandled_exception(); // Exception handling
};
};
2. Awaitable Pattern
2.1 Basic Awaitable Interface
struct MyAwaitable {
bool await_ready() const noexcept; // ready now? skip suspend
void await_suspend(std::coroutine_handle<> h); // park & arrange resume
ReturnType await_resume() noexcept; // value returned by co_await
};
2.2 Awaitable Examples
Simple Delay Awaitable
struct DelayAwaitable {
std::chrono::milliseconds delay;
bool await_ready() const noexcept { return delay.count() <= 0; }
void await_suspend(std::coroutine_handle<> h) {
// Schedule resumption after delay
std::thread([h, delay = this->delay]() {
std::this_thread::sleep_for(delay);
h.resume();
}).detach();
}
void await_resume() noexcept {}
};
Thread Pool Awaitable
struct ThreadPoolAwaitable {
Executor* executor;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
executor->submit(h);
}
void await_resume() noexcept {}
};
3. Task Implementation
3.1 Basic Task Type
struct Task {
struct promise_type {
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; } // lazy start
std::suspend_always final_suspend() noexcept { return {}; } // keep alive
void return_void() noexcept {}
void unhandled_exception() { throw; }
};
std::coroutine_handle<promise_type> h;
explicit Task(std::coroutine_handle<promise_type> handle) : h(handle) {}
~Task() { if (h) h.destroy(); }
// Move-only
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
Task(Task&& other) noexcept : h(std::exchange(other.h, {})) {}
Task& operator=(Task&& other) noexcept {
if (this != &other) {
if (h) h.destroy();
h = std::exchange(other.h, {});
}
return *this;
}
void start() { if (h) h.resume(); }
bool done() const { return h.done(); }
};
3.2 Detached Task (Fire-and-Forget)
struct DetachedTask {
struct promise_type {
DetachedTask get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; } // start immediately
struct Cleaner {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
h.destroy(); // self-destroy
}
void await_resume() noexcept {}
};
Cleaner final_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
};
4. Thread Pool Executor
class Executor {
public:
explicit Executor(unsigned threads = std::thread::hardware_concurrency()) {
stop_ = false;
for (unsigned i = 0; i < threads; ++i) {
workers_.emplace_back([this] { this->run(); });
}
}
~Executor() {
{
std::lock_guard<std::mutex> lk(mu_);
stop_ = true;
}
cv_.notify_all();
for (auto& t : workers_) t.join();
}
void submit(std::coroutine_handle<> h) {
{
std::lock_guard<std::mutex> lk(mu_);
q_.push(h);
}
cv_.notify_one();
}
private:
void run() {
while (true) {
std::coroutine_handle<> h{};
{
std::unique_lock<std::mutex> lk(mu_);
cv_.wait(lk, [this] { return stop_ || !q_.empty(); });
if (stop_) break;
h = q_.front();
q_.pop();
}
if (h) h.resume();
}
}
std::vector<std::thread> workers_;
std::queue<std::coroutine_handle<>> q_;
std::mutex mu_;
std::condition_variable cv_;
bool stop_;
};
5. SystemC Integration
5.1 SystemC Event Awaitable
#ifdef USE_SYSTEMC
#include <systemc.h>
struct EventAwaitable {
const sc_event& event;
bool await_ready() const noexcept {
// Check if event already triggered this delta cycle
return false; // Typically always suspend for SystemC events
}
void await_suspend(std::coroutine_handle<> h) {
// Schedule resumption when event triggers
// Method 1: Using SystemC's sensitivity mechanism
auto process = sc_spawn([h]() { h.resume(); });
sc_spawn_options opts;
opts.spawn_method();
opts.set_sensitivity(&event);
opts.dont_initialize();
}
void await_resume() noexcept {}
};
// Usage in SystemC module
SC_MODULE(CoroutineModule) {
sc_event ready_event;
SC_CTOR(CoroutineModule) {
SC_THREAD(coroutine_process);
}
void coroutine_process() {
auto task = example_coroutine();
task.start();
// SystemC simulation continues...
}
Task example_coroutine() {
std::cout << "Coroutine started\n";
// Wait for SystemC event
co_await EventAwaitable{ready_event};
std::cout << "Event triggered, continuing...\n";
co_return;
}
};
#endif
5.2 SystemC Time Awaitable
#ifdef USE_SYSTEMC
struct TimeAwaitable {
sc_time delay;
bool await_ready() const noexcept { return delay == SC_ZERO_TIME; }
void await_suspend(std::coroutine_handle<> h) {
// Schedule resumption after delay
auto process = sc_spawn([h]() { h.resume(); });
sc_spawn_options opts;
opts.spawn_method();
process.next_trigger(delay);
}
void await_resume() noexcept {}
};
// Usage: co_await TimeAwaitable{sc_time(10, SC_NS)};
#endif
6. Advanced Patterns
6.1 Generator (Lazy Sequences)
template<typename T>
struct Generator {
struct promise_type {
T current_value{};
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) noexcept {
current_value = std::move(value);
return {};
}
void return_void() noexcept {}
void unhandled_exception() { throw; }
};
std::coroutine_handle<promise_type> h;
explicit Generator(std::coroutine_handle<promise_type> handle) : h(handle) {}
~Generator() { if (h) h.destroy(); }
// Iterator interface
struct Iterator {
std::coroutine_handle<promise_type> h;
Iterator& operator++() {
h.resume();
return *this;
}
const T& operator*() const { return h.promise().current_value; }
bool operator!=(std::default_sentinel_t) const { return !h.done(); }
};
Iterator begin() {
h.resume(); // Prime the generator
return Iterator{h};
}
std::default_sentinel_t end() { return {}; }
};
// Usage
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
6.2 Async Value Producer/Consumer
template<typename T>
struct Future {
struct promise_type {
std::optional<T> result;
std::coroutine_handle<> waiter{};
Future get_return_object() {
return Future{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) {
result = std::move(value);
if (waiter) waiter.resume();
}
void unhandled_exception() { throw; }
};
std::coroutine_handle<promise_type> h;
explicit Future(std::coroutine_handle<promise_type> handle) : h(handle) {}
~Future() { if (h) h.destroy(); }
struct Awaiter {
std::coroutine_handle<promise_type> h;
bool await_ready() const noexcept {
return h.done();
}
void await_suspend(std::coroutine_handle<> waiter) {
h.promise().waiter = waiter;
}
T await_resume() {
return std::move(*h.promise().result);
}
};
Awaiter operator co_await() { return Awaiter{h}; }
};
7. Best Practices
7.1 Memory Management
- RAII: Always use RAII for coroutine handles
- Move semantics: Make coroutine types move-only
- Lifetime: Ensure coroutine frame outlives all references
- Cleanup: Always destroy handles to free coroutine frames
7.2 Performance
- Avoid allocations: Use custom allocators in promise_type
- Pool coroutines: Reuse coroutine frames when possible
- Stack-free: Coroutines don't use stack space when suspended
- Zero-cost: Well-optimized coroutines have minimal overhead
7.3 Debugging
- Handle validity: Always check handle validity before use
- Exception safety: Implement proper exception handling
- Logging: Add trace logging for coroutine lifecycle
- Testing: Test suspension/resumption edge cases
7.4 SystemC Integration
- Determinism: Ensure deterministic execution in SystemC context
- Event handling: Properly integrate with SystemC event system
- Time management: Respect SystemC time advancement
- Process spawning: Use SystemC process spawning for integration
8. Common Pitfalls
- Dangling handles: Destroying coroutine while handles exist
- Double destruction: Destroying the same handle twice
- Stack corruption: Accessing stack variables after suspension
- Exception propagation: Unhandled exceptions in promise_type
- Race conditions: Concurrent access to coroutine state
- Memory leaks: Forgetting to destroy coroutine handles
C++20 coroutines provide powerful abstractions for asynchronous programming, but require careful attention to lifetime management and integration patterns.