Coroutine C++20
Înțelegeți corutinele ca funcții suspendabile. Aflați co_yield, co_return, co_wait, modelul generatorului și cazuri practice de utilizare, cum ar fi I/O asincron și mașini de stare.
Ce sunt corutinele?
O corutină este o funcție care își poate suspenda execuția și poate fi reluată ulterior. Spre deosebire de funcțiile obișnuite care rulează de la început până la sfârșit, corutinele pot întrerupe execuția la mijloc, pot da rezultate intermediare și pot continua acolo unde au rămas. Acest lucru este fundamental diferit de fire de execuție: corutinele sunt cooperante (ele aleg când să suspende), ușoare (nu este nevoie de fire de sistem de operare) și deterministe (fără curse de date de la accesul simultan).
C++20 introduce suport pentru coroutine la nivel de limbă cu trei cuvinte cheie noi: co_yield (suspend și produce o valoare), co_return (completează coroutine cu o valoare finală) și co_await (suspend până la finalizarea unei operațiuni asincrone). Orice funcție care conține unul dintre aceste cuvinte cheie este automat o rutină.
Modelul generatorului
Un generator este o corutine care produce leneș o succesiune de valori. Fiecare apel cătreco_yieldsuspendă corotina și oferă o valoare apelantului. Iată o implementare completă, compilabilă a generatorului:
#include <iostream>
#include <coroutine>
#include <optional>
// A simple Generator type
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() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// Move only
Generator(Generator&& other) noexcept : handle(other.handle) { other.handle = nullptr; }
Generator& operator=(Generator&&) = delete;
Generator(const Generator&) = delete;
bool next() {
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().current_value; }
};
// A coroutine that generates Fibonacci numbers
Generator<long long> fibonacci() {
long long a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// A coroutine that generates a range
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
int main() {
auto fib = fibonacci();
std::cout << "First 10 Fibonacci numbers: ";
for (int i = 0; i < 10 && fib.next(); ++i) {
std::cout << fib.value() << " ";
}
std::cout << "\n"; // 0 1 1 2 3 5 8 13 21 34
auto r = range(1, 6);
while (r.next()) {
std::cout << r.value() << " "; // 1 2 3 4 5
}
std::cout << "\n";
return 0;
}Mașini Coroutine: Promisiune și mâner
Every coroutine has two key components:
promise_type — Controls the coroutine's behavior. The compiler looks for a nestedpromise_typein the coroutine's return type. Key methods:
-get_return_object()— creates the object returned to the caller
-initial_suspend()— returnssuspend_always(lazy start) orsuspend_never(eager start)
-final_suspend()— what to do when the coroutine ends (must benoexcept)
-yield_value(T)— called onco_yield
-return_value(T)orreturn_void()— called onco_return
-unhandled_exception()— called if an exception escapes
std::coroutine_handle — A type-erased pointer to the coroutine frame. Providesresume(),done(), anddestroy()methods. The coroutine frame lives on the heap and contains all local variables and the suspension point.
The flow is: caller creates coroutine ->initial_suspend-> caller callsresume()-> coroutine runs untilco_yield/co_return/co_await-> control returns to caller -> repeat.
co_wait și Awaitable Objects
co_awaitsuspends a coroutine until an asynchronous operation completes. Any type that implements the Awaitable interface can be used withco_await. This is the foundation for async I/O frameworks:
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
// A simple awaitable that simulates async work
struct AsyncTimer {
std::chrono::milliseconds duration;
bool await_ready() const noexcept {
return duration.count() <= 0; // skip suspend if no wait needed
}
void await_suspend(std::coroutine_handle<> handle) const {
// In production, this would register with an event loop
// Here we simulate with a thread
std::thread([handle, d = duration]() {
std::this_thread::sleep_for(d);
handle.resume(); // resume the coroutine
}).detach();
}
void await_resume() const noexcept {
// Called when the coroutine resumes — return value becomes
// the result of the co_await expression
}
};
// The three Awaitable methods:
// 1. await_ready() — can we skip suspending? (optimization)
// 2. await_suspend() — what to do when suspending
// 3. await_resume() — what value to produce when resuming
// A Task type for async coroutines
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() { std::terminate(); }
};
};
Task async_work() {
std::cout << "Starting async work...\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 1 complete\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 2 complete\n";
}
int main() {
async_work();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return 0;
}Cazuri practice de utilizare
Coroutines excel in several real-world scenarios:
Generators / Lazy Sequences — Produce values on demand without computing the entire sequence upfront. Ideal for infinite sequences, file parsing, and data pipelines.
Async I/O — Network servers, database queries, and file operations canco_awaitcompletion without blocking threads. Libraries likecppcoro,libunifex, and Boost.Asio provide coroutine-ready I/O primitives.
State Machines — Eachco_yieldorco_awaitrepresents a state transition. The coroutine's local variables naturally preserve state between transitions, eliminating manual state structs.
Event-Driven Programming — UI frameworks and game loops can use coroutines to express sequential logic that spans multiple frames or events.
Parsing — Tokenizers and parsers canco_yieldtokens or AST nodes as they process input, maintaining parsing state implicitly.
Coroutines and threads solve different problems:
Use coroutines when: you need cooperative multitasking, lazy evaluation, or async I/O without the overhead of OS threads. Coroutines have no context-switch overhead, no synchronization primitives needed, and can scale to millions of concurrent tasks.
Use threads when: you need true parallelism across CPU cores for CPU-bound work, or when you need preemptive scheduling (a coroutine that never suspends will block its thread).
Best practice: Use coroutines for I/O-bound concurrency and threads for CPU-bound parallelism. Many high-performance servers combine both: a thread pool runs coroutines, where each coroutine handles one connection usingco_awaitfor I/O.
Dangling references: Coroutine parameters are copied into the frame, but references to temporaries can dangle. Always take parameters by value or ensure the referenced object outlives the coroutine.
Heap allocation: Every coroutine allocates a frame on the heap. For very hot, short-lived coroutines, this can be a performance concern (though compilers can optimize via Heap Allocation eLision Optimization — HALO).
Debugging difficulty: Coroutine stacks are harder to inspect in debuggers. The suspended state is on the heap, not the call stack. Use logging and structured concurrency to maintain debuggability.
No standard library types: C++20 provides the language primitives but no standardGeneratororTasktype. You must write your own or use a library. C++23 addsstd::generator.
- Coroutines are suspendable functions — they pause with
co_yield/co_awaitand resume later - The generator pattern produces lazy sequences: each
co_yieldsuspends and delivers one value co_awaitsuspends until an async operation completes — the awaitable's three methods control the behavior- The
promise_typeandcoroutine_handleform the machinery that connects the coroutine to its caller - Coroutines are cooperative (no preemption) and lightweight (no OS thread) — ideal for I/O-bound concurrency
- C++20 provides primitives only; use
std::generator(C++23) or libraries like cppcoro for production code
Test — Verifică-ți cunoștințele
(20 XP)1. Which keyword makes a function a coroutine in C++20?
2. Care este scopul`promise_type::yield_value(T)`?
3. How do coroutines differ from threads?