C++20 Coroutines
Chápejte korutiny jako suspendovatelné funkce. Naučte se co_yield, co_return, co_await, vzor generátoru a praktické případy použití, jako jsou asynchronní I/O a stavové stroje.
Co jsou korutiny?
Corutina je funkce, která může pozastavit její provádění a obnovit později. Na rozdíl od běžných funkcí, které běží od začátku do konce, se korutiny mohou pozastavit uprostřed provádění, přinést průběžné výsledky a pokračovat tam, kde skončily. To se zásadně liší od vláken: korutiny jsou kooperativní (vybírají si, kdy se pozastaví), lehké (nepotřebuje vlákno OS) a deterministické (žádné závody s daty ze souběžného přístupu).
C++20 zavádí podporu korutiny na jazykové úrovni se třemi novými klíčovými slovy: co_yield (pozastavit a vytvořit hodnotu), co_return (doplňte korutinu konečnou hodnotou) a co_await (pozastavit, dokud se nedokončí asynchronní operace). Jakákoli funkce obsahující jedno z těchto klíčových slov je automaticky rutina.
Vzor generátoru
Generátor je korutina, která líně produkuje posloupnost hodnot. Každé voláníco_yieldpozastaví korutinu a doručí hodnotu volajícímu. Zde je kompletní, kompilovatelná implementace generátoru:
#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;
}Coroutine Machinery: Promise & Handle
Každý koroutin má dvě klíčové složky:
promise_type — Řídí chování korutiny. Kompilátor hledá vnořenýpromise_typev návratovém typu korutiny. Klíčové metody:
-get_return_object()— vytvoří objekt vrácený volajícímu
-initial_suspend()— vrátísuspend_always(líný start) nebosuspend_never(netrpělivý start)
-final_suspend()– co dělat, když koroutina skončí (musí býtnoexcept)
-yield_value(T)— voláno naco_yield
-return_value(T)neboreturn_void()– voláno naco_return
-unhandled_exception()— volá se, pokud unikne výjimka
std::coroutine_handle — Typově vymazaný ukazatel na rámec corutiny. Poskytuje metodyresume(),done()adestroy(). Korutinový rámec žije na hromadě a obsahuje všechny lokální proměnné a bod zavěšení.
Postup je: volající vytvoří korutinu ->initial_suspend-> volající zavoláresume()-> korutina běží, dokudco_yield/co_return/co_await-> řízení se vrátí volajícímu -> opakovat.
co_await a Awaitable Objects
co_awaitpozastaví korutinu, dokud se nedokončí asynchronní operace. Sco_awaitlze použít jakýkoli typ, který implementuje rozhraní Awaitable. Toto je základ pro asynchronní I/O frameworky:
#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;
}Praktické případy použití
Coroutines excelují v několika scénářích reálného světa:
Generátory / Lazy Sequences — Vytvářejte hodnoty na vyžádání, aniž byste museli předem počítat celou sekvenci. Ideální pro nekonečné sekvence, analýzu souborů a datové kanály.
Asynchronní I/O — Síťové servery, databázové dotazy a operace se soubory mohouco_awaitdokončit bez blokování vláken. Knihovny jakocppcoro,libunifexa Boost.Asio poskytují I/O primitiva připravená na koroutinu.
Stavové stroje — Každýco_yieldneboco_awaitpředstavuje přechod stavu. Lokální proměnné korutiny přirozeně zachovávají stav mezi přechody a eliminují manuální stavové struktury.
Programování řízené událostmi — Rámce uživatelského rozhraní a herní smyčky mohou používat korutiny k vyjádření sekvenční logiky, která zahrnuje více snímků nebo událostí.
Parsování – Tokenizéry a analyzátory mohou tokenyco_yieldnebo uzly AST při zpracování vstupu a implicitně udržovat stav analýzy.
Korutiny a vlákna řeší různé problémy:
Používejte rutiny, když: potřebujete kooperativní multitasking, pomalé vyhodnocování nebo asynchronní I/O bez režie vláken OS. Korutiny nemají režii přepínání kontextu, nejsou potřeba žádná synchronizační primitiva a mohou se škálovat na miliony souběžných úloh.
Používejte vlákna, když: potřebujete skutečný paralelismus mezi jádry CPU pro práci vázanou na CPU nebo když potřebujete preventivní plánování (korutina, která se nikdy nepozastaví, zablokuje své vlákno).
Osvědčený postup: Použijte korutiny pro souběžnost vázanou na I/O a vlákna pro paralelismus vázaný na CPU. Mnoho vysoce výkonných serverů kombinuje obojí: fond vláken spouští koroutiny, kde každá koroutina zpracovává jedno připojení pomocíco_awaitpro I/O.
Dangling reference: Coroutine parametry jsou zkopírovány do rámce, ale odkazy na provizoria mohou viset. Vždy berte parametry podle hodnoty nebo zajistěte, aby odkazovaný objekt přežil korutinu.
Alokace haldy: Každá koroutina přiděluje snímek na haldě. U velmi horkých korutin s krátkou životností to může představovat problém s výkonem (ačkoli kompilátory mohou optimalizovat pomocí Heap Allocation eLision Optimization — HALO).
Obtížnost ladění: V debuggerech se hůře kontrolují zásobníky Coroutine. Pozastavený stav je na haldě, nikoli na zásobníku volání. Pro zachování laditelnosti použijte protokolování a strukturovanou souběžnost.
Žádné standardní typy knihoven: C++20 poskytuje jazyková primitiva, ale žádný standardní typGeneratorneboTask. Musíte si napsat vlastní nebo použít knihovnu. C++23 přidávástd::generator.
- Korutiny jsou funkce, které lze pozastavit – pozastaví se pomocí
co_yield/co_awaita obnoví se později - The generator pattern produces lazy sequences: each
co_yieldsuspends and delivers one value co_awaitse pozastaví, dokud se nedokončí asynchronní operace – chování řídí tři metody čekánípromise_typeacoroutine_handletvoří stroj, který spojuje korutinu s jejím volajícím- Coroutines are cooperative (no preemption) and lightweight (no OS thread) — ideal for I/O-bound concurrency
- C++20 poskytuje pouze primitiva; pro produkční kód použijte
std::generator(C++23) nebo knihovny jako cppcoro
Kvíz — Otestujte své znalosti
(20 XP)1. Které klíčové slovo dělá z funkce rutinu v C++20?
2. Jaký je účel`promise_type::yield_value(T)`?
3. How do coroutines differ from threads?