Skip to main content
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 ready
  • co_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

  1. Dangling handles: Destroying coroutine while handles exist
  2. Double destruction: Destroying the same handle twice
  3. Stack corruption: Accessing stack variables after suspension
  4. Exception propagation: Unhandled exceptions in promise_type
  5. Race conditions: Concurrent access to coroutine state
  6. 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.