coroutines
asynchronous programming
C++
concurrency
software development

Coroutine usages for asynchronous programming in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

C++20 coroutines let you write asynchronous code that reads like synchronous code. A coroutine is a function that can suspend execution at specific points (co_await, co_yield, co_return) and resume later without blocking a thread. This eliminates callback nesting and manual state machines for async I/O, generators, and lazy sequences. Unlike std::async or std::thread, coroutines do not create new threads — they provide cooperative multitasking on existing threads.

Basic Coroutine Structure

cpp
1#include <coroutine>
2#include <iostream>
3
4// A simple Task type that wraps a coroutine
5struct Task {
6    struct promise_type {
7        Task get_return_object() { return {}; }
8        std::suspend_never initial_suspend() { return {}; }
9        std::suspend_never final_suspend() noexcept { return {}; }
10        void return_void() {}
11        void unhandled_exception() { std::terminate(); }
12    };
13};
14
15Task sayHello() {
16    std::cout << "Hello, ";
17    co_await std::suspend_always{};  // Suspend here
18    std::cout << "World!\n";         // Resume here
19}

A function becomes a coroutine when it uses co_await, co_yield, or co_return. The compiler transforms it into a state machine that can be paused and resumed.

co_await: Asynchronous Operations

cpp
1#include <coroutine>
2#include <future>
3#include <iostream>
4
5// Simplified async task
6struct AsyncTask {
7    struct promise_type {
8        int result;
9        AsyncTask get_return_object() {
10            return {std::coroutine_handle<promise_type>::from_promise(*this)};
11        }
12        std::suspend_never initial_suspend() { return {}; }
13        std::suspend_always final_suspend() noexcept { return {}; }
14        void return_value(int value) { result = value; }
15        void unhandled_exception() { std::terminate(); }
16    };
17
18    std::coroutine_handle<promise_type> handle;
19
20    int getResult() { return handle.promise().result; }
21    ~AsyncTask() { if (handle) handle.destroy(); }
22};
23
24AsyncTask fetchData() {
25    // Simulate async work
26    co_return 42;
27}
28
29AsyncTask processData() {
30    auto task = fetchData();
31    int value = task.getResult();
32    co_return value * 2;
33}

co_yield: Generators

Generators produce values lazily — each value is computed on demand:

cpp
1#include <coroutine>
2#include <iostream>
3
4template<typename T>
5struct Generator {
6    struct promise_type {
7        T current_value;
8        Generator get_return_object() {
9            return {std::coroutine_handle<promise_type>::from_promise(*this)};
10        }
11        std::suspend_always initial_suspend() { return {}; }
12        std::suspend_always final_suspend() noexcept { return {}; }
13        std::suspend_always yield_value(T value) {
14            current_value = value;
15            return {};
16        }
17        void return_void() {}
18        void unhandled_exception() { std::terminate(); }
19    };
20
21    std::coroutine_handle<promise_type> handle;
22
23    bool next() {
24        handle.resume();
25        return !handle.done();
26    }
27    T value() { return handle.promise().current_value; }
28    ~Generator() { if (handle) handle.destroy(); }
29};
30
31Generator<int> fibonacci() {
32    int a = 0, b = 1;
33    while (true) {
34        co_yield a;
35        auto next = a + b;
36        a = b;
37        b = next;
38    }
39}
40
41int main() {
42    auto fib = fibonacci();
43    for (int i = 0; i < 10 && fib.next(); i++) {
44        std::cout << fib.value() << " ";
45    }
46    // Output: 0 1 1 2 3 5 8 13 21 34
47}

The generator produces Fibonacci numbers one at a time, computing each only when next() is called.

Async I/O Pattern

cpp
1// Conceptual async file reader using coroutines
2struct FileReadAwaitable {
3    std::string filename;
4    std::string result;
5
6    bool await_ready() { return false; }  // Always suspend
7
8    void await_suspend(std::coroutine_handle<> handle) {
9        // Submit async I/O operation
10        // When I/O completes, resume the coroutine:
11        // io_service.async_read(filename, [handle](auto data) {
12        //     result = data;
13        //     handle.resume();
14        // });
15    }
16
17    std::string await_resume() { return result; }
18};
19
20AsyncTask processFile() {
21    auto content = co_await FileReadAwaitable{"data.txt"};
22    // Execution continues here after file is read
23    // No blocking, no callback nesting
24    co_return content.size();
25}

The coroutine suspends at co_await, the I/O operation runs asynchronously, and the coroutine resumes when data is ready — all without blocking a thread.

Coroutines vs Callbacks vs Threads

cpp
1// CALLBACK STYLE — nested, hard to follow
2void processData_callbacks() {
3    readFile("input.txt", [](auto data) {
4        parseData(data, [](auto parsed) {
5            writeFile("output.txt", parsed, [](auto result) {
6                std::cout << "Done: " << result << "\n";
7            });
8        });
9    });
10}
11
12// COROUTINE STYLE — reads top to bottom
13AsyncTask processData_coroutine() {
14    auto data = co_await readFileAsync("input.txt");
15    auto parsed = co_await parseDataAsync(data);
16    auto result = co_await writeFileAsync("output.txt", parsed);
17    std::cout << "Done: " << result << "\n";
18    co_return;
19}

Coroutines linearize async code. Error handling works with normal try/catch instead of error callbacks.

Lazy Evaluation with Coroutines

cpp
1Generator<int> range(int start, int end) {
2    for (int i = start; i < end; i++) {
3        co_yield i;
4    }
5}
6
7Generator<int> filter(Generator<int> gen, auto predicate) {
8    while (gen.next()) {
9        if (predicate(gen.value())) {
10            co_yield gen.value();
11        }
12    }
13}
14
15// Only computes values as needed
16auto evens = filter(range(0, 1000000), [](int x) { return x % 2 == 0; });
17for (int i = 0; i < 5 && evens.next(); i++) {
18    std::cout << evens.value() << " ";
19}
20// Output: 0 2 4 6 8
21// Only computed 9 values from range, not 1,000,000

Library Support

cpp
1// cppcoro library (most popular coroutine utility library)
2// Install: vcpkg install cppcoro
3
4#include <cppcoro/task.hpp>
5#include <cppcoro/sync_wait.hpp>
6#include <cppcoro/when_all.hpp>
7
8cppcoro::task<int> fetchA() { co_return 1; }
9cppcoro::task<int> fetchB() { co_return 2; }
10
11cppcoro::task<int> fetchBoth() {
12    auto [a, b] = co_await cppcoro::when_all(fetchA(), fetchB());
13    co_return a + b;
14}
15
16int main() {
17    int result = cppcoro::sync_wait(fetchBoth());
18    std::cout << result << "\n";  // 3
19}

cppcoro provides task<T>, generator<T>, when_all, when_any, and other utilities that make C++20 coroutines practical.

Common Pitfalls

  • No standard library Task type: C++20 provides the coroutine machinery (co_await, co_yield, co_return) but no standard Task or Generator type. You must write your own or use a library like cppcoro.
  • Dangling coroutine handles: If you destroy a coroutine_handle while it is suspended, any references to stack variables in the coroutine become invalid. Always ensure proper lifetime management.
  • Coroutines are not threads: co_await does not create a new thread. If no executor schedules the resume on another thread, the coroutine resumes on whatever thread calls handle.resume(). For true parallelism, integrate with a thread pool.
  • Heap allocation: Coroutine state is allocated on the heap by default. For performance-critical code, the compiler can sometimes elide this allocation (HALO optimization), but it is not guaranteed.
  • Compiler support varies: GCC, Clang, and MSVC all support C++20 coroutines but with varying levels of optimization and debugging support. Test on your target compiler.

Summary

  • C++20 coroutines use co_await, co_yield, and co_return to write suspendable functions
  • Generators (co_yield) produce values lazily without computing the entire sequence upfront
  • Async I/O (co_await) eliminates callback nesting by letting you write linear async code
  • Coroutines do not create threads — they provide cooperative multitasking on existing threads
  • Use a library like cppcoro for practical Task and Generator types
  • The promise_type pattern lets you customize suspension, return values, and exception handling

Course illustration
Course illustration

All Rights Reserved.