Какова механика сопрограмм в С++ 20?

Я пытался прочитать документацию (cppreference и стандартную документацию по самой функции) о последовательности операций, которые вызываются при вызове, приостановке, возобновлении и прекращении функции сопрограммы. Документация углубленно описывает различные точки расширения, которые позволяют разработчикам библиотек настраивать поведение своих сопрограмм с использованием компонентов библиотеки. На высоком уровне эта языковая особенность, кажется, очень хорошо продумана.

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

Следующие функции находятся в наборе новых точек настройки, которые я не совсем понимаю:

  • initial_suspend()
  • return_void()
  • return_value()
  • await_ready()
  • await_suspend()
  • await_resume()
  • final_suspend()
  • unhandled_exception()

Может кто-нибудь описать в высокоуровневом psuedocode код, который генерирует компилятор при запуске сопрограммы пользователя? На абстрактном уровне я пытаюсь выяснить, когда вызываются такие функции, как await_suspend, await_resume, await_ready, await_transform, return_value и т.д., Для чего они служат и как я могу их использовать для писать сопрограммные библиотеки.


Не уверен, что это не по теме, но некоторые вводные ресурсы здесь будут чрезвычайно полезны для сообщества в целом. Поиск в Google и погружение в реализацию библиотек, как в cppcoro, не помогает мне преодолеть этот первоначальный барьер :(

Ответы

Ответ 1

N4775 описывает предложение сопрограмм для C++ 20. Он вводит ряд разных идей. Следующее из моего блога на https://dwcomputersolutions.net. Больше информации можно найти в других моих постах.

Прежде чем мы рассмотрим всю программу сопрограмм Hello World, пройдите различные детали шаг за шагом. К ним относятся:

  1. Обещание сопрограммы
  2. Контекст сопрограммы
  3. Будущее сопрограммы
  4. Ручка сопрограммы
  5. Сама сопрограмма
  6. Подпрограмма, которая фактически использует сопрограмму

Весь файл включен в конце этого пост.

Сопрограмма

Future f()
{
    co_return 42;
}

Мы создаем нашу сопрограмму с помощью

    Future myFuture = f();

Это простая сопрограмма, которая просто возвращает значение 42. Это сопрограмма потому что он включает в себя ключевое слово co_return. Любая функция, которая имеет ключевые слова co_await, co_return или co_yield - сопрограмма.

Первое, что вы заметите, это то, что, хотя мы возвращаем целое число, тип возврата сопрограммы (определенный пользователем) тип Future. Причина в том, что когда мы вызываем нашу сопрограмму, мы не запускаем функцию прямо сейчас, скорее мы инициализировать объект, который в конечном итоге даст нам значение, которое мы ищем Ака наше будущее.

Нахождение обещанного типа

Когда мы создаем нашу сопрограмму, первое, что делает компилятор, - это находит Тип обещания, который представляет этот конкретный тип сопрограммы.

Мы сообщаем компилятору, какой тип обещания принадлежит какой функции сопрограммы подпись путем создания шаблона частичной специализации для

template <typename R, typename P...>
struct coroutine_trait
{};

with a member called 'promise_type' that defines our Promise Type

В нашем примере мы могли бы использовать что-то вроде:

template<>
struct std::experimental::coroutines_v1::coroutine_traits<Future> {
    using promise_type = Promise;
};

Здесь мы создаем специализацию coroutine_trait без указания параметров и тип возврата Future, он точно соответствует сигнатуре нашей функции сопрограммы Future f(void). promise_type - это тип обещания, который в нашем случае struct Promise.

Теперь мы пользователь, мы обычно не будем создавать свои собственные coroutine_trait специализация, так как библиотека сопрограмм обеспечивает хороший простой способ укажите promise_type в самом классе Future. Подробнее об этом позже.

Контекст сопрограммы

Как упоминалось в моем предыдущем посте, потому что сопрограммы могут быть приостановлены и возобновляемые локальные переменные не всегда могут быть сохранены в стеке. Хранить нестабильные локальные переменные, компилятор будет размещать объект Context на куча Будет также сохранен экземпляр нашего Обещания.

Обещание, будущее и ручка

Сопрограммы в основном бесполезны, если они не могут общаться с внешний мир. Наше обещание говорит нам, как должна вести себя сопрограмма, в то время как наш Будущий объект позволяет другому коду взаимодействовать с сопрограммой. Обещание и Будущее затем общаться друг с другом через нашу ручку сопрограммы.

Обещание

Простое сопрограммное обещание выглядит примерно так:

struct Promise 
{
    Promise() : val (-1), done (false) {}
    std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
    std::experimental::coroutines_v1::suspend_always final_suspend() {
        this->done = true;
        return {}; 
    }
    Future get_return_object();
    void unhandled_exception() { abort(); }
    void return_value(int val) {
        this->val = val;
    }

    int val;
    bool done;    
};

Future Promise::get_return_object()
{
    return Future { Handle::from_promise(*this) };
}

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

После этого компилятор вызывает get_return_object Эта пользовательская функция затем отвечает за создание объекта Future и возвращение его сопрограммный инициатор.

В нашем случае мы хотим, чтобы наше будущее могло общаться с нашими сопрограммами. поэтому мы создаем наше будущее с ручкой для нашей сопрограммы. Это позволит нашим Будущее, чтобы получить доступ к нашему обещанию.

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

В нашем случае, так как мы хотим, чтобы функция запускалась немедленно, мы вызываем suspend_never. Если мы приостановили функцию, нам нужно было бы запустить сопрограммы, вызвав метод возобновления на дескрипторе.

Нам нужно знать, что делать, когда оператор co_return вызывается в сопрограмма. Это делается с помощью функции return_value. В этом случае мы сохранить значение в Обещании для последующего извлечения через Future.

В случае исключения нам нужно знать, что делать. Это сделано Функция unhandled_exception. Поскольку в нашем примере исключения не должны случается, мы просто прерываем.

Наконец, нам нужно знать, что делать, прежде чем уничтожить нашу сопрограмму. Это сделано через final_suspend function В этом случае, так как мы хотим получить результат, поэтому мы возвращаемся suspend_always. Сопрограмма должна быть уничтожена с помощью метода сопрограммы destroy. В противном случае, если мы вернемся suspend_never сопрограмма уничтожает себя, как только заканчивает работу.

Ручка

Ручка дает доступ к сопрограмме, а также его обещание. Есть два ароматы, пустая ручка, когда нам не нужно получить доступ к обещанию и сопрограмма с типом обещания, когда нам нужно получить доступ к обещанию.

template <typename _Promise = void>
class coroutine_handle;

template <>
class coroutine_handle<void> {
public:
    void operator()() { resume(); }
    //resumes a suspended coroutine
    void resume();
    //destroys a suspended coroutine
    void destroy();
    //determines whether the coroutine is finished
    bool done() const;
};

template <Promise>
class coroutine_handle : public coroutine_handle<void>
{
    //gets the promise from the handle
    Promise& promise() const;
    //gets the handle from the promise
    static coroutine_handle from_promise(Promise& promise) no_except;
};

Будущее

Будущее выглядит так:

class [[nodiscard]] Future
{
public:
    explicit Future(Handle handle)
        : m_handle (handle) 
    {}
    ~Future() {
        if (m_handle) {
            m_handle.destroy();
        }
    }
    using promise_type = Promise;
    int operator()();
private:
    Handle m_handle;    
};

int Future::operator()()
{
    if (m_handle && m_handle.promise().done) {
        return m_handle.promise().val;
    } else {
        return -1;
    }
}

Объект Future отвечает за абстрагирование сопрограммы вовне Мир. У нас есть конструктор, который берет ручку из обещания согласно обещание реализации get_return_object.

Деструктор уничтожает сопрограмму, так как в нашем случае это будущее, которое контролировать время обещания.

наконец, у нас есть строка:

using promise_type = Promise;

Библиотека C++ избавляет нас от реализации нашего собственного coroutine_trait, как мы это сделали выше, если мы определим наш promise_type в классе возврата сопрограммы.

И там у нас это есть. Наша самая первая простая сопрограмма.

Полный источник



#include <experimental/coroutine>
#include <iostream>

struct Promise;
class Future;

using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;

struct Promise 
{
    Promise() : val (-1), done (false) {}
    std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
    std::experimental::coroutines_v1::suspend_always final_suspend() {
        this->done = true;
        return {}; 
    }
    Future get_return_object();
    void unhandled_exception() { abort(); }
    void return_value(int val) {
        this->val = val;
    }

    int val;
    bool done;    
};

class [[nodiscard]] Future
{
public:
    explicit Future(Handle handle)
        : m_handle (handle) 
    {}
    ~Future() {
        if (m_handle) {
            m_handle.destroy();
        }
    }
    using promise_type = Promise;
    int operator()();
private:
    Handle m_handle;    
};

Future Promise::get_return_object()
{
    return Future { Handle::from_promise(*this) };
}


int Future::operator()()
{
    if (m_handle && m_handle.promise().done) {
        return m_handle.promise().val;
    } else {
        return -1;
    }
}

//The Co-routine
Future f()
{
    co_return 42;
}

int main()
{
    Future myFuture = f();
    std::cout << "The value of myFuture is " << myFuture() << std::endl;
    return 0;
}

Awaiters

Оператор co_await позволяет нам приостановить нашу сопрограмму и вернуть контроль обратно к вызывающему сопрограмму. Это позволяет нам выполнять другую работу в ожидании завершения нашей операции. Когда они закончат, мы можем возобновить их с именно там, где мы остановились.

Оператор co_await может обработать выражение несколькими способами. справа от него. Сейчас мы рассмотрим простейший случай, и именно здесь наш co_await выражение возвращает Awaiter.

Ожидатель - это простой struct или class, который реализует следующее методы: await_ready, await_suspend и await_resume.

bool await_ready() const {...} просто возвращает, готовы ли мы возобновить сопрограммы или нам нужно посмотреть на приостановку нашей сопрограммы. Если предположить, await_ready возвращает ложь. Переходим к запуску await_suspend

Для метода await_suspend доступно несколько подписей. Самым простым является void await_suspend(coroutine_handle<> handle) {...}. Это ручка для объект сопрограммы, который наш co_await приостановит. Как только эта функция завершится, управление возвращается обратно вызывающей стороне объекта сопрограммы. Именно эта функция который отвечает за хранение ручки сопрограммы на потом, чтобы наши сопрограмма не может быть приостановлена навсегда.

Однажды handle.resume() вызывается; await_ready возвращает ложь; или какой-то другой механизм возобновляет нашу сопрограмму, вызывается метод auto await_resume(). возвращаемое значение из await_resume - это значение, которое возвращает оператор co_await. Иногда для expr в co_await expr нецелесообразно возвращать ожидающего как описано выше. Если expr возвращает класс, класс может предоставить свой собственный экземпляр Awaiter operator co_await (...) which will return the Awaiter. Alternatively one can implement an await_transform method in our promise_type ', который преобразует expr в Awaiter.

Теперь, когда мы сняли с Awaiter, я хотел бы отметить, что Методы initial_suspend и final_suspend в нашем promise_type оба возвращают Awaiters. Объект suspend_always и suspend_never являются тривиальными ожидающими. suspend_always возвращает true для await_ready, а suspend_never возвращает ложный. Ничто не мешает вам раскрутить свои собственные.

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