coroutines
asynchronous programming
C++
concurrency
software development

Coroutine usages for asynchronous programming in C

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions