Последнее обновление:2026-10-12
Введение в корутины C++20¶
1.Что такое Coroutines¶
Начиная с C++20 язык C++ наконец поддерживает корутины; C++23 должен принести дополнительную поддержку и охватить, как минимум, самые распространённые сценарии использования.
Корутина (англ. coroutine) — это класс компонентов компьютерных программ, обобщающий подпрограммы в духе кооперативной многозадачности и допускающий приостановку и возобновление выполнения. По сравнению с подпрограммами корутины более общие и гибкие, но на практике применяются не так широко, как подпрограммы. Корутины лучше подходят для реализации таких знакомых программных компонентов, как кооперативная многозадачность, обработка исключений, событийный цикл, итераторы, бесконечные списки и конвейеры.
Абзац из Википедии, описывающий основную идею корутин.
С точки зрения C++ корутину можно просто понимать как любую функцию, содержащую co_return, co_yield или co_await.
Сейчас корутины в C++20 — это синтаксический сахар над функциональными объектами: компилятор генерирует вокруг корутины каркас кода. Этот код опирается на определяемые пользователем promise types. Возможно, после C++23 в этом что-то изменится, но до тех пор будем действовать по правилам C++20 и писать эти типы самостоятельно.
Ранее упомянутые co_return, co_yield, co_await будут разобраны в последующих разделах.
2.Step By Step Coroutine¶
Далее начнём с простого примера корутины: следующая корутина не делает ничего, назовём её корутиной HelloTask.
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
HelloTask myCoroutine() {
co_return; // make it a coroutine
}
int main() {
HelloTask x = myCoroutine();
}
Скомпилируем и запустим этот код:
Требуется, чтобы ваш компилятор поддерживал возможности C++20.
Пройдёмся сверху вниз по этой HelloWorld-программе и посмотрим, чем она отличается от привычной разработки.
2.1 Заголовочный файл¶
Прежде всего, подключается заголовочный файл coroutine; я возьму напрямую исходники gcc-10 и посмотрю, что же там делается.
https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/libstdc++-v3/include/std/coroutine
Содержимое включает следующее:
// 用于发现协程promise_type的特征类型
// 核心代码:using promise_type = typename _Result::promise_type;
1.coroutine_traits
// 协程句柄,用于管理暂停或执行的协程以及Promise
2.coroutine_handle
// coroutine_handle全特化版本 是其他特化的公开基类。它保有handle的底层 地址。
2.1 coroutine_handle<void>
// coroutine_handle泛化版本
2.2 template <typename _Promise>
struct coroutine_handle : coroutine_handle<>
// coroutine_handle全特化版本 处理无操作协程
2.3 template <>
struct coroutine_handle<noop_coroutine_promise> : public coroutine_handle<>
3.平凡多可等待对象 Trivial awaitables
// await表达式应该暂停
3.1 suspend_always
// await表达式不应该暂停
3.2 suspend_never
Видно, что внутри много специальных терминов:
- coroutine_handle
- Promise
- awaitable
и так далее.
Всё дальнейшее изложение корутин строится вокруг этих понятий.
2.2 Promise¶
Продолжим разбор HelloTask и обнаружим в нём такой фрагмент:
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Возникает вопрос: зачем здесь писать promise_type?
promise_type нужен потому, что мы упомянули handle в разделе 2.1 выше: когда компилятор создаёт handle корутины, необходимо указать promise_type объекта этой корутины, поэтому практически у любого объекта корутины есть структура HelloTask::promise_type. Внимательные читатели наверняка уже всё поняли: promise_type передаётся туда, внутри Handle он разбирается, и так определяется поведение пользовательского объекта корутины.
Посмотрим глубже, что есть внутри promise_type: его содержимое фиксировано; по мере дальнейшего изложения вовлечённых функций будет становиться всё больше — например, здесь используется return_void, а когда позже co_return начнёт возвращать объект, потребуется уже другая функция.
get_return_object может вернуть текущий пользовательский объект; здесь никаких действий не производится. Продолжая тему раздела 2.1, для этого можно использовать coroutine_handle.
struct promise_type {
using Handle = std::coroutine_handle<promise_type>;
HelloTask get_return_object() {
return HelloTask{Handle::from_promise(*this)};
}
// other member function
}
Видно, что здесь с помощью члена from_promise у coroutine_handle конструируется объект.
initial_suspend: сообщает компилятору, должна ли корутина оставаться приостановленной при первом выполнении, или же говорит, остаётся ли корутина приостановленной при инстанцировании.
final_suspend: в противовес initial_suspend, после завершения тела корутины, в её конце вызывается final_suspend. Это даёт возможность выполнить дополнительную очистку.
Обе возвращают awaitable; здесь используется suspend_always из заголовочного файла, но можно и определить пользовательский awaitable — об этом будет рассказано в последующих статьях.
Исходный код suspend_always и suspend_never таков:
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never
{
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
Здесь содержится несколько членов:
- await_ready
- Если известно, что результат уже готов, завершиться ли синхронно?
- await_suspend
- Возвращаемым значением может быть bool, void или coroutine handle.
- Здесь возвращается void — управление немедленно возвращается текущему caller/resumer корутины (сама корутина остаётся приостановленной); если же await_suspend возвращает bool, то true означает передачу управления caller/resumer, а false — возобновление текущей корутины. О возвращении coroutine handle будет рассказано в последующих статьях.
- await_resume
- Его результат — результат всего выражения co_await. Поэтому возвращаемое значение не обязано быть void, им может быть и пользовательский объект и т. п.
return_void: при использовании co_return; вызывается return_void.
unhandled_exception: используется для перехвата исключений.
Наконец, остаётся последний шаг — создание корутины; согласно определению в начале, корутина — это любая функция, содержащая co_return, co_yield или co_await. Отсюда и появляется:
2.3 Общий поток¶
Каков весь поток вызовов? В этом разделе подробно разберём логику выполнения приведённого выше кода; перед началом изменим код, добавив несколько операций print:
#include <iostream>
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
void return_void() { std::cout << "return_void" << std::endl; }
void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
};
};
HelloTask myCoroutine() {
std::cout << "create coroutine" << std::endl;
co_return; // make it a coroutine
}
int main() {
std::cout << "start main" << std::endl;
HelloTask x = myCoroutine();
std::cout << "end main" << std::endl;
}
Вывод при этом:
Поток таков:
- start main
- В куче выделяется объект корутины, конструируется promise, вызывается promise.get_return_object, возвращающий текущий объект (в этот момент выводится get_return_object); цель этого шага — сохранить результат в локальной переменной.
- promise.initial_suspend — в этот момент выводится initial_suspend; здесь срабатывает co_await, выводится create coroutine, после выполнения тела корутины вызывается co_return.
- co_return вызывает promise.return_void, выводя return_void.
- Наконец, promise.final_suspend выводит final_suspend.
- end main
О корутинах есть ещё очень много материала; этот раздел — лишь начало, далее буду продолжать обновлять~
Похожие темы¶
- Практика с channel в C++20 | C++那些事
- Концепции Concepts в C++20 | C++那些事
- constexpr и оператор «запятая» в C++20 | C++那些事
- Порядок байтов endian в C++20 | C++那些事
- Лямбда-выражения в C++20 (часть 1) | C++那些事
- Модули Modules в C++20 | C++那些事
- Библиотека Ranges в C++20 | C++那些事
- Дизайн и подводные камни std::span в C++20 | C++那些事
- Обзор новых возможностей C++20 | C++那些事
- [[likely]] и [[unlikely]] в C++20 | C++那些事