C++20 Корутины
Корутина понимается как приостанавливаемая функция. Узнайте co yield, co return, co await, шаблон генератора и практические варианты использования, такие как асинхронизация ввода-вывода и государственные машины.
Что такое коровы?
coroutine — функция, которая может приостановить его выполнение и быть возобновлена позже. В отличие от обычных функций, которые выполняются от начала до конца, корутины могут приостанавливаться в середине исполнения, давать промежуточные результаты и продолжать там, где они остановились. Это принципиально отличается от потоков: корутины являются кооперативными (они выбирают, когда приостановить), легкими (не нужны нити ОС) и детерминированными (нет гонок данных из параллельного доступа).
C++20 представляет поддержку корутина на уровне языка с тремя новыми ключевыми словами: co_yield (приостановить и произвести значение), co_return (завершить корутин с окончательным значением) и co_await (приостановить до завершения операции асинхронизации). Любая функция, содержащая одно из этих ключевых слов, автоматически является рутиной.
Модель генератора
Генератор — это корутин, который лениво производит последовательность значений. Каждый звонок в co_yield приостанавливает корутин и доставляет значение абоненту. Вот полная, компилируемая реализация генератора:
#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;
}Оригинальное название: Promise & Handle
Каждый корутин имеет два ключевых компонента:
promise_type — Контролирует поведение корутина. Компилятор ищет вложенный promise_type в типе возврата корутина. Ключевые методы:
- get_return_object() - создает объект, возвращенный абоненту
- initial_suspend() - возвращает suspend_always (ленивый старт) или suspend_never (жесткий старт)final_suspend() — что делать, когда заканчивается корутин (должен быть noexcept)
— yield_value(T) — называется co_yield
- return_value(T) или return_void() - называется co_return
- unhandled_exception() - называется, если исключение ускользает
std::coroutine_handle — стертый шрифтом указатель на корутинную рамку. Методы resume(), done() и destroy(). Корутинная рамка живет на куче и содержит все локальные переменные и точку подвески.
Поток такой: вызывающий создает корутин -> initial_suspend -> Caller calls resume() -> Coroutine работает до co_yield/co_return/co_await -> контроль возвращается абоненту -> повторение.
Co await и ожидаемые объекты
co_await приостанавливает корутин до завершения асинхронной операции. Любой тип, который реализует интерфейс Awaitable, может быть использован с co_await. Это основа для фреймворков ввода/вывода асинхронизации:
#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;
}Случаи практического использования
Корутины превосходят в нескольких реальных сценариях:
Генераторы/ленивые последовательности — производят значения по требованию без предварительного вычисления всей последовательности. Идеально подходит для бесконечных последовательностей, анализа файлов и конвейеров данных.
Async I/O — сетевые серверы, запросы к базам данных и файловые операции co_await завершение без блокировки нитей. Библиотеки как cppcoro, libunifexИ увеличить. Asio предоставляет готовые к корутину I/O примитивы.
Государственные машины — каждый co_yield или co_await представляет собой государственный переход. Местные переменные корутина естественным образом сохраняют состояние между переходами, устраняя ручные структуры состояния.
Event-Driven Programming — фреймворки пользовательского интерфейса и игровые циклы могут использовать кодуины для выражения последовательной логики, которая охватывает несколько кадров или событий.
Парсинг — токенизаторы и парсеры могут co_yield токены или узлы AST по мере их обработки ввода, неявно сохраняя состояние парсинга.
Корутины и нити решают разные задачи:
Используйте корутин, когда : вам нужна совместная многозадачность, ленивая оценка или асинхронизация ввода/вывода без накладных расходов на потоки ОС. Корутины не имеют накладных расходов на переключение контекста, не требуются примитивы синхронизации и могут масштабироваться до миллионов одновременных задач.
Используйте потоки, когда **: вам нужен истинный параллелизм между ядрами ЦП для работы, связанной с ЦП, или когда вам нужно предварительное планирование (коротин, который никогда не приостанавливается, будет блокировать его поток).
Наилучшая практика: Используйте корутин для I/O-связанной параллели и потоков для CPU-связанного параллелизма. Многие высокопроизводительные серверы объединяют оба: пул потоков запускает корутины, где каждый корутин обрабатывает одно соединение с использованием co_await для ввода/вывода.
Нечеткие ссылки: Корутинные параметры копируются в кадр, но ссылки на временные могут болтаться. Всегда берите параметры по значению или убедитесь, что объект, на который делается ссылка, пережил корутин.
Распределение расходов: Каждый корутин выделяет каркас на кучу. Для очень горячих, недолговечных корутин это может быть проблемой производительности (хотя компиляторы могут оптимизировать с помощью оптимизации распределения кучи eLision — HALO).
Сложность отладки: Корутинные стопки сложнее проверить в отладчиках. Подвесное состояние находится на куче, а не в стеке вызовов. Используйте журналирование и структурированную параллель для поддержания отладки.
Нет типов стандартных библиотек : C++20 предоставляет языковые примитивы, но нет стандартных типов Generator или Task. Вы должны написать свой собственный или использовать библиотеку. C++23 добавляет std::generator.
- Корутины являются приостанавливаемыми функциями — они приостанавливаются с
co_yield/co_awaitи возобновляются позже. - Модель генератора производит ленивые последовательности: каждый
co_yieldприостанавливает и обеспечивает одно значение. co_awaitприостанавливается до завершения операции асинхронизации — три метода ожидаемого контролируют поведение.promise_typeиcoroutine_handleобразуют механизм, который соединяет корутин с абонентом.- Корутины являются кооперативными (без упреждения) и легкими (без нити ОС) - идеально подходят для сопряженной с I / O параллели
- C++20 предоставляет только примитивы; используйте
std::generator(C++23) или библиотеки, такие как cppcoro для производственного кода
Тест — проверьте свои знания
(20 XP)1. Какое ключевое слово делает функцию корутиной в C++20?
2. Что такое `promise_type::yield_value(T)`?
3. Чем корутины отличаются от нитей?