Přeskočit na obsah

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:

generator.cpp
#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:

co_await_example.cpp
#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.

Osvědčený postup

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.

Úskalí

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.

Klíčové poznatky
  • Korutiny jsou funkce, které lze pozastavit – pozastaví se pomocíco_yield/co_awaita obnoví se později
  • The generator pattern produces lazy sequences: eachco_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žijtestd::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?