cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

Стандартная библиотека корутин #498

Open kelbon opened 2 years ago

kelbon commented 2 years ago

Описание: Нам очевидно нужна поддержка корутин в стандарте. Минимальный набор готовых корутин, инфраструктура вокруг них(utility), концепты, набор "сделай сам" для кастомизации под конкретные нужды.

Примеры Смешной и неэффективный пример, потому что все итак знают, что генераторы полезны


template<std::ranges::borrowed_range... Ranges>
auto zip(Ranges&&... rs)->generator<std::tuple<decltype(std::declval<Ranges>()[0])...>> {
  for (size_t i = 0; ((i < rs.size()) && ...); ++i)
      co_await yield(rs[i]...);
      // yield это специальный тип, когда хочется yield значение
      // сконструировать прямо тут из многих аргументов
      // (иначе это воспринимается как init list)
}

void zip_user {
  std::vector<char> vec;
  vec.resize(10, 'c');
  std::string sz = "Hello world";
  for (auto [a,b,c] : zip(vec, sz, std::string_view(sz)))
      std::format_to(std::ostream_iterator<char>(std::cout), "{}{}{}\n", a, b, c);
}

Нужны корутины также и для асинхронного кода, конечно же

kelbon::logical_thread Multithread(std::atomic<int32_t>& value, std::allocator<std::byte>) {
// В этом примере я игнорирую стоп токен, но вообще можно его использовать,
// хотя всё равно по умолчанию на каждом co_await проверяется запрошен ли стоп
  auto my_stop_token = co_await this_coro::stop_token;
  co_await jump_on(another_thread);
  for (auto i : std::views::iota(0, 100)) ++value; // do my job
}

void Moo() {
  std::atomic<int32_t> value;
  std::vector<logical_thread> workers;
  for (int i = 0; i < 10; ++i)
    workers.emplace_back(Multithread(value, std::allocator<std::byte>{}));
// здесь вызывается деструктор вектора, который вызывает деструкторы корутин
// и мы дожидаемся выполнения всех логических потоков
}

<Код c реализацией вашей идеи, если есть> Есть https://github.com/kelbon/UndefinedBehavior_GoldEdition

apolukhin commented 2 years ago

Есть библиотека, которую начинали стандартизировать https://github.com/lewissbaker/cppcoro

Есть несколько альтернативных библиотек, например https://github.com/David-Haim/concurrencpp

Если действительно готовы взяться за идею и довести её до конца, то стоит начать с изучения предложений от Lewiss Baker и описания того, как улучшить его наработки.

kelbon commented 2 years ago

Есть библиотека, которую начинали стандартизировать https://github.com/lewissbaker/cppcoro

К сожалению она не выглядит как поддерживаемая на данный момент. Там очень много смелых идей, скажем fmap внутри генератора, но сейчас по сути std::views::transform, аналогично наработки cancellation token/ source, которые теперь std::stop_source/ std::stop_token, то есть многие вещи устарели Или скажем "концепт" awaiter из cppcoro

template<typename T>
struct is_awaiter<T, std::void_t<
    decltype(std::declval<T>().await_ready()),
    decltype(std::declval<T>().await_suspend(std::declval<std::experimental::coroutine_handle<>>())),
    decltype(std::declval<T>().await_resume())>> :
        std::conjunction<
        std::is_constructible<bool, decltype(std::declval<T>().await_ready())>,
        detail::is_valid_await_suspend_return_value<
            decltype(std::declval<T>().await_suspend(std::declval<std::experimental::coroutine_handle<>>()))>>
{};

Просто нерабочий, т.к. фактически запрещает awaiter принимать в await_suspend любой тип кроме std::coroutine_handle\<void> (неявная конвертация есть только в одну сторону, плюс возможны ситуации где используются методы, которых нет в std::coroutine_handle\<void> или стоит requires, отсекающий \<void> специализацию и т.д.(эта же проблема есть в пропозале task от Гора Нишанова, не знаю исправлена ли она сейчас) ) cppcoro также практически не предоставляет никаких инструментов для написания своих корутин, даже внутри библиотеки каждый из типов корутин описан индивидуально с нуля, не поддержаны аллокаторы / memory resources. В целом cppcoro выглядит как набор смелых идей, но не проглядывается общей концепции и системного подхода, скажем async_generator\<T>, (пример из cppcoro):


cppcoro::async_generator<int> ticker(int count, threadpool& tp)
{
  for (int i = 0; i < count; ++i)
  {
    co_await tp.delay(std::chrono::seconds(1));
    co_yield i;
  }
}

cppcoro::task<> consumer(threadpool& tp)
{
  auto sequence = ticker(10, tp);
  for co_await(std::uint32_t i : sequence)
  {
    std::cout << "Tick " << i << std::endl;
  }
}

Лично я не вижу причин делать это вообще асинхронной операцией, если оба потока вынуждены друг друга ждать в итоге, а как использовать этот генератор иначе - непонятно. С другой стороны можно было бы представить библиотеку, которая даёт базу для создания прикладных корутин и я наследуясь от базовой корутины лишь доопределяю один await_transform для того чтобы действительно безопасно и удобно делать yield в нужный мне контейнер. И нет, с async_generator так не получится, т.к. тип корутины состоит из двух типов - промиса и типа-владельца, промолчу уж про возможную поломку внутренней логики при шадоувинге методов промиса. Придётся переписывать всё с нуля.

Также я смотрел предложения по std::generator, главные претензии к нему - неявные для пользователя крайне костыльные шаблонные аргументы, тайп ерейз для поддержки аллокаторов, требование именно аллокатора, хотя в данной ситуации требования явно должны быть меньше, проблемы с ссылками. Главное, что все эти проблемы выдуманные. Они все решаемы. Декларация моего генератора:

template <typename Yield, typename MemoryResource = std::allocator<std::byte>>
struct generator;

ясные для пользователя шаблонные аргументы, никакого тайп ерайза, 0 оверхеда на аллокации, если аллокатор stateless(то есть мы не платим за то, чего не используем), никаких проблем с ссылками, потому что значения не "выбрасываются" из генератора, а "показываются", то есть при co_yield Lvalue; потребителю будет показываться именно этот объект lvalue, а при co_yield Rvalue; создаваться объект и хранится до следующего пробуждения генератора, при этом on consumer side это всегда выглядит как взаимодействие с lvalue, у этого есть множество интересных применений, это снижает оверхед и вероятность вылетания исключения(для lvalue очевидно всегда noexcept), а также позволяет создавать генераторы ссылок без каких либо странных вещей и проблем с лайфтаймами. Можно сделать из генератора хранилище, почему нет?

generator<int> ViewGenerator() {
    int x = 1;
    int y = 2;
    while (x < 100) {
        co_yield ++x;
        co_yield y;
    }
}