Spec-Zone.ru › C++

Корутины (C++20)

Корутина — это функция, которая может приостанавливать выполнение, чтобы возобновить его позже. Корутины являются бесклеточные: они приостанавливают выполнение, возвращаясь к вызывающей функции, а данные, необходимые для возобновления выполнения, хранятся отдельно от стека. Это позволяет использовать последовательный код, выполняющийся асинхронно (например, для обработки ввода-вывода без блокировок без явных обратных вызовов), а также поддерживает алгоритмы над ленивыми вычислениями бесконечных последовательностей и другие применения.

Функция является корутиной, если её определение содержит одно из следующих:

  • выражение co_await — для приостановки выполнения до возобновления
task<> tcp_echo_server()
{
    char data[1024];
    while (true)
    {
        std::size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
    }
}
  • выражение co_yield — для приостановки выполнения с возвратом значения
generator<unsigned int> iota(unsigned int n = 0)
{
    while (true)
        co_yield n++;
}
  • оператор co_return — для завершения выполнения с возвратом значения
lazy<int> f()
{
    co_return 7;
}

Каждая корутина должна иметь тип возврата, удовлетворяющий ряду требований, указанных ниже.

Ограничения

Корутины не могут использовать переменное количество аргументов, обычные операторы return или заполнитель типа возврата (auto или Концепт).

Функции consteval, функции constexpr, конструкторы, деструкторы и главная функция не могут быть корутинами.

Выполнение

Каждая корутина связана с

  • объектом promise, манипулируемым изнутри корутины. Корутина отправляет своё результат или исключение через этот объект.
  • обработкой корутины, манипулируемой извне корутины. Это ссылка, не владеющая ресурсом, используемая для возобновления выполнения корутины или для уничтожения её фрейма.
  • состоянием корутины, которое является внутренним динамически выделенным хранилищем (если выделение не оптимизировано), объект, содержащий
    • объект promise
    • параметры (все скопированы по значению)
    • некоторое представление текущей точки приостановки, чтобы возобновление знало, где продолжить, а уничтожение знало, какие локальные переменные были в области видимости
    • локальные переменные и временные объекты, чья продолжительность жизни охватывает текущую точку приостановки.

Когда корутина начинает выполнение, она выполняет следующие действия:

  • выделяет объект состояния корутины с помощью operator new.
  • копирует все параметры функции в состояние корутины: параметры по значению перемещаются или копируются, параметры по ссылке остаются ссылками (следовательно, могут стать висячими, если корутина возобновляется после того, как закончится продолжительность жизни объекта-ссылка — см. примеры ниже).
  • вызывает конструктор объекта promise. Если тип promise имеет конструктор, принимающий все параметры корутины, вызывается этот конструктор с параметрами корутины после копирования. В противном случае вызывается конструктор по умолчанию.
  • вызывает promise.get_return_object() и сохраняет результат в локальной переменной. Результат этого вызова будет возвращён вызывающей функции, когда корутина впервые приостановится. Любые исключения, сгенерированные до этого шага и включая его, передаются обратно вызывающей функции, а не помещаются в promise.
  • вызывает promise.initial_suspend() и co_await свой результат. Типичные типы Promise либо возвращают std::suspend_always, для ленивых корутин, либо std::suspend_never, для жадных корутин.
  • когда корутина co_await promise.initial_suspend() возобновляется, начинается выполнение тела корутины.

Примеры того, как параметр становится висячим:

#include <coroutine>
#include <iostream>
 
struct promise;
 
struct coroutine : std::coroutine_handle<promise>
{
    using promise_type = ::promise;
};
 
struct promise
{
    coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
};
 
struct S
{
    int i;
    coroutine f()
    {
        std::cout << i;
        co_return;
    }
};
 
void bad1()
{
    coroutine h = S{0}.f();
    // S{0} destroyed
    h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
    h.destroy();
}
 
coroutine bad2()
{
    S s{0};
    return s.f(); // returned coroutine can't be resumed without committing use after free
}
 
void bad3()
{
    coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
    {
        std::cout << i;
        co_return;
    }(); // immediately invoked
    // lambda destroyed
    h.resume(); // uses (anonymous lambda type)::i after free
    h.destroy();
}
 
void good()
{
    coroutine h = [](int i) -> coroutine // make i a coroutine parameter
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda destroyed
    h.resume(); // no problem, i has been copied to the coroutine
                // frame as a by-value parameter
    h.destroy();
}

Когда корутина достигает точки приостановки

  • объект возврата, полученный ранее, возвращается вызывающей функции/возобновляющему коду после неявного преобразования к типу возврата корутины, если это необходимо.

Когда корутина достигает оператора co_return, она выполняет следующее:

  • вызывает promise.return_void() для
    • co_return;
    • co_return expr; где expr имеет тип void
  • или вызывает promise.return_value(expr) для co_return expr; где expr имеет не-void тип
  • уничтожает все переменные со статическим временем жизни в обратном порядке их создания.
  • вызывает promise.final_suspend() и co_await результат.

Выход из корутины без оператора co_return эквивалентен co_return;, за исключением того, что поведение является неопределённым, если нет объявлений return_void в области видимости Promise. Функция без указанных ключевых слов в своём теле не является корутиной, независимо от типа возврата, и выход без co_return приводит к неопределённому поведению, если тип возврата не является (возможно, с модификатором cv) void.

// assuming that task is some coroutine task type
task<void> f()
{
    // not a coroutine, undefined behavior
}
 
task<void> g()
{
    co_return;  // OK
}
 
task<void> h()
{
    co_await g();
    // OK, implicit co_return;
}

Если корутина завершается с необработанным исключением, она выполняет следующие действия:

  • перехватывает исключение и вызывает promise.unhandled_exception() из блока catch
  • вызывает promise.final_suspend() и co_await результат (например, для возобновления продолжения или публикации результата). Возобновление корутины с этой точки является неопределённым поведением.

Когда состояние корутины уничтожается либо из-за завершения с помощью co_return или необработанного исключения, либо из-за уничтожения через её обработку, она выполняет следующие действия:

  • вызывает деструктор объекта promise.
  • вызывает деструкторы копий параметров функции.
  • вызывает operator delete для освобождения памяти, используемой состоянием корутины.
  • возвращает выполнение обратно вызывающей функции/возобновляющему коду.

Динамическое выделение

Состояние корутины выделяется динамически с помощью не-массивнового operator new.

Если тип Promise определяет замену на уровне класса, она будет использована, в противном случае будет использован глобальный operator new.

Если тип Promise определяет размещаемую форму operator new, которая принимает дополнительные параметры, и они соответствуют списку аргументов, где первый аргумент — запрашиваемый размер (типа std::size_t), а остальные — аргументы функции корутины, эти аргументы будут переданы в operator new (это позволяет использовать соглашение о ведущем аллокаторе для корутин).

Вызов operator new может быть оптимизирован (даже если используется пользовательский аллокатор), если

  • Продолжительность жизни состояния корутины строго вложена в продолжительность жизни вызывающей функции, и
  • размер фрейма корутины известен в месте вызова.

В этом случае состояние корутины встраивается в фрейм стека вызывающей функции (если вызывающая функция — обычная функция) или в состояние корутины (если вызывающая функция — корутина).

Если выделение не удалось, корутина генерирует std::bad_alloc, если тип Promise не определяет функцию-член Promise::get_return_object_on_allocation_failure(). Если эта функция-член определена, выделение использует небросковый вариант operator new, а в случае неудачи выделения корутина сразу возвращает объект, полученный из Promise::get_return_object_on_allocation_failure() вызывающей функции, например:

struct Coroutine::promise_type
{
    /* ... */
 
    // ensure the use of non-throwing operator-new
    static Coroutine get_return_object_on_allocation_failure()
    {
        std::cerr << __func__ << '\n';
        throw std::bad_alloc(); // or, return Coroutine(nullptr);
    }
 
    // custom non-throwing overload of new
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // allocation failure
    }
};

Promise

Тип Promise определяется компилятором по типу возврата корутины с использованием std::coroutine_traits.

Формально, пусть R и Args... обозначают тип возврата и список параметров корутины соответственно, ClassT и cv-qual (если есть) обозначают тип класса, к которому принадлежит корутина, и его cv-квалификацию соответственно, если это определена как нестатический член-функция, её тип Promise определяется:

  • std::coroutine_traits<R, Args...>::promise_type, если корутина не определена как нестатический член-функция,
  • std::coroutine_traits<R, ClassT /*cv-qual*/&, Args...>::promise_type, если корутина определена как нестатический член-функция, не являющаяся ссылкой на rvalue,
  • std::coroutine_traits<R, ClassT /*cv-qual*/&&, Args...>::promise_type, если корутина определена как нестатический член-функция, являющаяся ссылкой на rvalue.

Например:

Если корутина определена как ... то её тип Promise ...
task<void> foo(int x); std::coroutine_traits<task<void>, int>::promise_type
task<void> Bar::foo(int x) const; std::coroutine_traits<task<void>, const Bar&, int>::promise_type
task<void> Bar::foo(int x) &&; std::coroutine_traits<task<void>, Bar&&, int>::promise_type

co_await

Унарный оператор co_await приостанавливает корутину и возвращает управление вызывающей функции. Его операнд — выражение, которое либо (1) имеет тип класса, определяющего член-оператор co_await, или может быть передано в нечленный оператор co_await, или (2) преобразуемо к такому типу класса с помощью Promise::await_transform текущей корутины.

co_await expr

Выражение co_await может появляться только в выражении, потенциально вычисляемом внутри обычного тела функции, и не может появляться

  • в обработчике исключений,
  • в операторе объявления, за исключением случая, когда оно появляется в инициализаторе этого оператора объявления,
  • в простом операторе объявления init-statement (см. if, switch, for и range-for), за исключением случая, когда оно появляется в инициализаторе этого init-statement,
  • в аргументе по умолчанию или
  • в инициализаторе переменной со статическим или потоковым временем жизни.

Сначала expr преобразуется в объект await как следует:

  • если expr получено от начальной точки приостановки, конечной точки приостановки или выражения yield, то awaitable — это expr в неизменённом виде.
  • в противном случае, если у текущего корутина Promise типа есть член-функция await_transform, то awaitable — это promise.await_transform(expr).
  • в противном случае, awaitable — это expr в неизменённом виде.

Затем объект awaiter получается следующим образом:

  • если разрешение перегрузки для оператора co_await даёт единственную лучшую перегрузку, awaiter — результат этого вызова:
    • awaitable.operator co_await() для перегрузки члена,
    • operator co_await(static_cast<Awaitable&&>(awaitable)) для перегрузки не члена.
  • в противном случае, если разрешение перегрузки не находит оператор co_await, awaiter — awaitable в неизменённом виде.
  • в противном случае, если разрешение перегрузки является неоднозначным, программа некорректна.

Если выражение выше является prvalue, объект awaiter является временным объектом, материализованным из него. В противном случае, если выражение выше является glvalue, объект awaiter — это объект, на который оно ссылается.

Затем вызывается awaiter.await_ready() (это быстрый способ избежать затрат на приостановку, если известно, что результат готов или может быть завершён синхронно). Если его результат, контекстно преобразованный в bool, равен false, то

Корутина приостанавливается (состояние корутины заполняется локальными переменными и текущей точкой приостановки).
Вызывается awaiter.await_suspend(handle), где handle — дескриптор корутины, представляющий текущую корутину. Внутри этой функции состояние приостановленной корутины наблюдается через этот дескриптор, и именно эта функция отвечает за её планирование для возобновления на некотором исполнителе или за её уничтожение (возвращение false считается планированием)
  • если await_suspend возвращает void, управление немедленно возвращается вызывающему/возобновляющему текущую корутину (эта корутина остаётся приостановленной), иначе
  • если await_suspend возвращает bool,
    • значение true возвращает управление вызывающему/возобновляющему текущую корутину
    • значение false возобновляет текущую корутину.
  • если await_suspend возвращает дескриптор корутины для другой корутины, этот дескриптор возобновляется (вызовом handle.resume()) (это может привести к цепочке вызовов, в конечном итоге запустив текущую корутину).
  • если await_suspend выбрасывает исключение, исключение перехватывается, корутина возобновляется, и исключение немедленно повторно выбрасывается.

Наконец, вызывается awaiter.await_resume() (независимо от того, была ли приостановлена корутина или нет), и результат является результатом всего выражения co_await expr.

Если корутина была приостановлена в выражении co_await и позже возобновлена, точка возобновления находится непосредственно перед вызовом awaiter.await_resume().

Обратите внимание, что поскольку корутина полностью приостанавливается перед входом в awaiter.await_suspend(), эта функция свободна передавать дескриптор корутины через потоки без дополнительной синхронизации. Например, она может поместить его в обратный вызов, запланированный для выполнения в пуле потоков при завершении асинхронной операции ввода-вывода. В этом случае, поскольку текущая корутина может быть возобновлена и, следовательно, выполнить деструктор объекта awaiter, все одновременно, пока await_suspend() продолжает своё выполнение в текущем потоке, await_suspend() должен считать *this уничтоженным и не обращаться к нему после публикации дескриптора в другие потоки.

Пример

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out)
{
    struct awaitable
    {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Output jthread parameter not empty");
            out = std::jthread([h] { h.resume(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        void await_resume() {}
    };
    return awaitable{&out};
}
 
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() {}
    };
};
 
task resuming_on_new_thread(std::jthread& out)
{
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main()
{
    std::jthread out;
    resuming_on_new_thread(out);
}

Возможный вывод:

Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224

Примечание: объект awaiter является частью состояния корутины (как временный объект, срок жизни которого пересекает точку приостановки) и уничтожается до завершения выражения co_await. Он может использоваться для поддержания состояния каждой операции, как требуется некоторыми API асинхронного ввода-вывода, без использования дополнительных динамических выделений.

Библиотека стандартной библиотеки определяет две тривиальные awaitables: std::suspend_always и std::suspend_never.

Демонстрация promise_type::await_transform и предоставленного пользователем awaiter

Пример

#include <cassert>
#include <coroutine>
#include <iostream>
 
struct tunable_coro
{
    // An awaiter whose "readiness" is determined via constructor's parameter.
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // Three standard awaiter interface functions:
        bool await_ready() const noexcept { return ready_; }
        static void await_suspend(std::coroutine_handle<>) noexcept {}
        static void await_resume() noexcept {}
    };
 
    struct promise_type
    {
        using coro_handle = std::coroutine_handle<promise_type>;
        auto get_return_object() { return coro_handle::from_promise(*this); }
        static auto initial_suspend() { return std::suspend_always(); }
        static auto final_suspend() noexcept { return std::suspend_always(); }
        static void return_void() {}
        static void unhandled_exception() { std::terminate(); }
        // A user provided transforming function which returns the custom awaiter:
        auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); }
        void disable_suspension() { ready_ = false; }
    private:
        bool ready_{true};
    };
 
    tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); }
 
    // For simplicity, declare these 4 special functions as deleted:
    tunable_coro(tunable_coro const&) = delete;
    tunable_coro(tunable_coro&&) = delete;
    tunable_coro& operator=(tunable_coro const&) = delete;
    tunable_coro& operator=(tunable_coro&&) = delete;
 
    ~tunable_coro()
    {
        if (handle_)
            handle_.destroy();
    }
 
    void disable_suspension() const
    {
        if (handle_.done())
            return;
        handle_.promise().disable_suspension();
        handle_();
    }
 
    bool operator()()
    {
        if (!handle_.done())
            handle_();
        return !handle_.done();
    }
private:
    promise_type::coro_handle handle_;
};
 
tunable_coro generate(int n)
{
    for (int i{}; i != n; ++i)
    {
        std::cout << i << ' ';
        // The awaiter passed to co_await goes to promise_type::await_transform which
        // issues tunable_awaiter that initially causes suspension (returning back to
        // main at each iteration), but after a call to disable_suspension no suspension
        // happens and the loop runs to its end without returning to main().
        co_await std::suspend_always{};
    }
}
 
int main()
{
    auto coro = generate(8);
    coro(); // emits only one first element == 0
    for (int k{}; k < 4; ++k)
    {
        coro(); // emits 1 2 3 4, one per each iteration
        std::cout << ": ";
    }
    coro.disable_suspension();
    coro(); // emits the tail numbers 5 6 7 all at ones
}

Вывод:

0 1 : 2 : 3 : 4 : 5 6 7

co_yield

Выражение co_yield возвращает значение вызывающему объекту и приостанавливает текущую корутину: это общий строительный блок возобновляемых функций-генераторов.

co_yield expr
co_yield braced-init-list

Это эквивалентно

co_await promise.yield_value(expr)

Типичный yield_value генератора хранил бы (копировал/перемещал или просто хранил адрес, поскольку срок жизни аргумента пересекает точку приостановки внутри co_await ) свой аргумент в объект генератора и возвращал std::suspend_always, передавая управление вызывающему/возобновляющему.

#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
 
template<typename T>
struct Generator
{
    // The class name 'Generator' is our choice and it is not required for coroutine
    // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
    // You can use name 'MyGenerator' (or any other name) instead as long as you include
    // nested struct promise_type with 'MyGenerator get_return_object()' method.
 
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
 
    struct promise_type // required
    {
        T value_;
        std::exception_ptr exception_;
 
        Generator get_return_object()
        {
            return Generator(handle_type::from_promise(*this));
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { exception_ = std::current_exception(); } // saving
                                                                              // exception
 
        template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from)
        {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
        }
        void return_void() {}
    };
 
    handle_type h_;
 
    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool()
    {
        fill(); // The only way to reliably find out whether or not we finished coroutine,
                // whether or not there is going to be a next value generated (co_yield)
                // in coroutine via C++ getter (operator () below) is to execute/resume
                // coroutine until the next co_yield point (or let it fall off end).
                // Then we store/cache result in promise to allow getter (operator() below
                // to grab it without executing coroutine).
        return !h_.done();
    }
    T operator()()
    {
        fill();
        full_ = false; // we are going to move out previously cached
                       // result to make promise empty again
        return std::move(h_.promise().value_);
    }
 
private:
    bool full_ = false;
 
    void fill()
    {
        if (!full_)
        {
            h_();
            if (h_.promise().exception_)
                std::rethrow_exception(h_.promise().exception_);
            // propagate coroutine exception in called context
 
            full_ = true;
        }
    }
};
 
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
    if (n == 0)
        co_return;
 
    if (n > 94)
        throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
 
    co_yield 0;
 
    if (n == 1)
        co_return;
 
    co_yield 1;
 
    if (n == 2)
        co_return;
 
    std::uint64_t a = 0;
    std::uint64_t b = 1;
 
    for (unsigned i = 2; i < n; ++i)
    {
        std::uint64_t s = a + b;
        co_yield s;
        a = b;
        b = s;
    }
}
 
int main()
{
    try
    {
        auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
 
        for (int j = 0; gen; ++j)
            std::cout << "fib(" << j << ")=" << gen() << '\n';
    }
    catch (const std::exception& ex)
    {
        std::cerr << "Exception: " << ex.what() << '\n';
    }
    catch (...)
    {
        std::cerr << "Unknown exception.\n";
    }
}

Вывод:

fib(0)=0
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34

Примечания

Макрокоманда проверки наличия функции Значение Std Функция
__cpp_impl_coroutine 201902L (C++20) Корутины (поддержка компилятора)
__cpp_lib_coroutine 201902L (C++20) Корутины (поддержка библиотеки)
__cpp_lib_generator 202207L (C++23) std::generator: синхронный генератор корутин для диапазонов

Поддержка библиотеки

Библиотека поддержки корутин определяет несколько типов, обеспечивающих компиляционную и исполняемую поддержку корутин.

Отчёты об ошибках

Следующие отчёты об ошибках, изменяющие поведение, были применены ретроактивно к ранее опубликованным стандартам C++.

DR Применён к Поведение, как опубликовано Правильное поведение
CWG 2556 C++20 неправильное return_void делало поведение
выхода за пределы корутины неопределённым
в этом случае программа является некорректной

См. также

generator
(C++23)
view-представление синхронного генератора корутин
(шаблон класса)

Внешние ссылки

1. David Mazières, 2021 - Руководство по корутинам C++20.
2. Lewis Baker, 2017-2022 - Асимметричная передача.

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/cpp/language/coroutines

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API