Перейти к содержанию

Последнее обновление: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();
}

Скомпилируем и запустим этот код:

g++ hello.cpp -fcoroutines -std=c++20

Требуется, чтобы ваш компилятор поддерживал возможности 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. Отсюда и появляется:

HelloTask myCoroutine() {
    co_return; // make it a coroutine
}

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
get_return_object
initial_suspend
create coroutine
return_void
final_suspend
end main

Поток таков:

  • 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

О корутинах есть ещё очень много материала; этот раздел — лишь начало, далее буду продолжать обновлять~

Похожие темы

Комментарии