Имплементация простых фьючерсов с помощью корутин

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Вместо return в корутине используется co_return, возвращающий результат. В этой заметке я хочу реализовать простую корутину с использованием co_return.

Возможно, вам будет интересно: Хотя я уже излагал теорию, лежащую в основе корутин, мне хочется еще раз об этом написать. Мой рассказ прост и основан на личном опыте. C++20 не предоставляет конкретные корутины, вместо этого он предлагает структуру для их реализации. Эта платформа состоит из более чем 20 функций, некоторые из которых вы должны имплементировать, а другие могут быть переопределены. На основе этих функций компилятор генерирует два рабочих процесса, которые определяют поведение корутины. Упрощенно говоря, корутины в C++20 — это обоюдоострый меч. С одной стороны, они предоставляют вам огромные возможности, с другой — они довольно сложны для понимания. Я посвятил корутинам более 80 страниц в своей книге "C++20: Узнай подробности", и то еще не все объяснил.

Из моего опыта, использование простых корутин и их модификация — это самый простой — возможно, единственный — способ понять их. Именно такого подхода я придерживаюсь в следующих статьях. Я представляю простые корутины и модифицирую их. Чтобы сделать рабочий процесс наглядным, в тексте содержится много комментариев и добавлено ровно столько теории, сколько необходимо для понимания внутренней сути корутин. Мои объяснения ни в коем случае не являются исчерпывающими и предназначены только для того, чтобы послужить отправной точкой в углублении ваших знаний о корутинах.

Краткое напоминание

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

Благодаря новым ключевым словам co_await и co_yield, C++20 расширяет выполнение функций C++ двумя новыми концепциями.

С помощью co_await expression можно приостанавливать и возобновлять выполнение выражения. Если вы используете co_await expression в функции func, вызов auto getResult = func() не блокируется, если результат вызова функции func() недоступен. Вместо потребляющей ресурсы блокировки осуществляется экономящее ресурсы ожидание.

Выражение co_yield позволяет реализовывать функции-генераторы. Генераторы — функции, которые возвращают новое значение с каждым последующим вызовом. Функция генератор является подобием потоков данных, из которых можно получать значения. Потоки данных могут быть бесконечными. Таким образом, данные концепции являются основополагающими для ленивых вычислений в C++.

Кроме того, корутина не выполняет return своего результата, она выполняет co_return своего результата.

// ...

MyFuture<int> createFuture() {
    co_return 2021;
}

int main() {

    auto fut = createFuture();
    std::cout << "fut.get(): " << fut.get() << '\n';

}  

В этом наглядном примере createFuture является корутиной, поскольку в ней используется одно из трех новых ключевых слов co_return, co_yield или co_await и она возвращает корутину MyFuture<int>. Что? Вот это зачастую озадачивало меня. Название корутина (coroutine) используется для двух сущностей. Позвольте мне ввести два новых термина. createFuture — это фабрика корутин, которая возвращает объект корутины fut, используемый для запроса результата: fut.get().

Этой теории должно быть достаточно. Давайте поговорим о co_return.

co_return

Признаться, корутина в программе eagerFuture.cpp - это самая простая корутина, которую можно себе представить, но которая все же делает что-то значимое: она автоматически сохраняет результат своего вызова.

// eagerFuture.cpp

#include <coroutine>
#include <iostream>
#include <memory>

template<typename T>
struct MyFuture {
    std::shared_ptr<T> value;                           // (3)
    MyFuture(std::shared_ptr<T> p): value(p) {}
    ~MyFuture() { }
    T get() {                                          // (10)
        return *value;
    }

    struct promise_type {
        std::shared_ptr<T> ptr = std::make_shared<T>(); // (4)
        ~promise_type() { }
        MyFuture<T> get_return_object() {              // (7)
            return ptr;
        }
        void return_value(T v) {
            *ptr = v;
        }
        std::suspend_never initial_suspend() {          // (5)
            return {};
        }
        std::suspend_never final_suspend() noexcept {  // (6)
            return {};
        }
        void unhandled_exception() {
            std::exit(1);
        }
    };
};

MyFuture<int> createFuture() {                         // (1)
    co_return 2021;                                    // (9)
}

int main() {

    std::cout << '\n';

    auto fut = createFuture();
    std::cout << "fut.get(): " << fut.get() << '\n';   // (2)

    std::cout << '\n';

}

MyFuture ведет себя как будущее, которое выполняется немедленно (см. "Асинхронные вызовы функций"). Вызов корутины createFuture (строка 1) возвращает будущее, а вызов fut.get (строка 2) забирает результат связанного промиса.

Есть одно тонкое различие с будущим: возвращаемое значение корутины createFuture доступно после ее вызова. Из-за проблем связанных с жизненным циклом корутины, она управляется std::shared_ptr (строки 3 и 4). Корутина всегда использует std::suspend_never (строки 5 и 6) и, таким образом, не приостанавливается ни перед запуском, ни после. Это означает, что корутина немедленно выполняется, когда вызывается функция createFuture. Функция-член get_return_object (строка 7) возвращает хэндл к корутине и сохраняет его в локальной переменной. return_value (строка 8) хранит результат работы корутины, который был предоставлен co_return 2021 (строка 9). Клиент вызывает fut.get (строка 2) и использует будущее в качестве дескриптора к промису. Функция-член get окончательно возвращает результат клиенту (строка 10).

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

На этом этапе я должен добавить немного теории.

Схема работы промиса

Когда вы используете co_yield, co_await или co_return в функции, то она становится корутиной, и компилятор преобразует тело функции в нечто эквивалентное следующим строкам.

{
  Promise prom;                      // (1)
  co_await prom.initial_suspend();   // (2)
  try {                                         
    <function body>                  // (3)
  }
  catch (...) {
    prom.unhandled_exception();
  }
FinalSuspend:
  co_await prom.final_suspend();     // (4)
}

Знакомы ли вам названия этих функций? Правильно! Это функции-члены внутреннего класса promise_type. Вот шаги, которые компилятор выполняет при создании объекта корутины в качестве возвращаемого значения фабрики корутины createFuture. Сначала он создает объект промис (строка 1), вызывает его функцию-член initial_suspend (строка 2), выполняет тело фабрики корутины (строка 3) и, наконец, вызывает функцию-член final_suspend (строка 4). Обе функции-члены initial_suspend и final_suspend в программе eagerFuture.cpp возвращают предопределенные awaitables (ожидающие) элементы std::suspend_never. Как следует из названия, этот awaitable (ожидаемый) объект никогда не приостанавливается и, следовательно, объект корутина также никогда не приостанавливается и ведет себя как обычная функция. Ожидание - это то, чего вы можете ждать. Оператору co_await требуется awaitable (возможность ожидания). В одном из следующих постов я напишу об awaitable и втором рабочем процессе awaiter.

На основании этого упрощенного рабочего процесса обещания можно сделать вывод о том, какие функции-члены нужны промису (promise_type) как минимум:

  • Конструктор по умолчанию

  • initial_suspend

  • final_suspend

  • unhandled_exception

Конечно, это далеко не полное объяснение, но, по крайней мере, достаточное для того, чтобы получить первое представление о рабочем процессе корутин.

Что дальше?

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


Материал подготовлен в рамках курса «C++ Developer. Professional».

Всех желающих приглашаем на открытый урок «C++20: Обзор нововведений».
Цели занятия:
- открыть для себя дивный мир последнего стандарта, о котором ходят столько легенд и слухов;
- объяснить, почему именно такие изменения были добавлены в стандарт;
- получить список нововведений для повседневного использования.
>> РЕГИСТРАЦИЯ

Источник: https://habr.com/ru/company/otus/blog/574440/


Интересные статьи

Интересные статьи

Когда мы начинаем изучать корутины, то «идём» и пробуем что-то простое с билдером runBlocking, поэтому многим он хорошо знаком. runBlocking запускает новую корутину, блок...
Вы используете семантический подход к управлению версиями? Вы используете gitflow? Скорее всего, вы знакомы с процессом корректировки версий, создания веток, слияния с master/dev, пов...
Здравствуйте. Решил поделится своей находкой — плодом раздумий, проб и ошибок. По большому счёту: это никакая не находка, конечно же — всё это должно быть давно известно, тем кто за...
Среди тем предстоящей конференции TechLead Conf 2020 будет детальное обсуждение Domain-Driven Design и EventStorming. Помимо подготовки 2-слотового доклада Константина Густова о DDD, доклада Серг...
Невозможно объективно измерить, какие девушки красивее: блондинки или брюнетки, смуглые или белокожие, высокие или миниатюрные. Но можно посчитать, какие черты внешности упоминают чаще, когда гов...