[Программирование, C++] Корутины C++20 в примерах (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Дек-2020 15:31
В преддверии старта курса "C++ Developer. Professional" приглашаем записаться на открытый вебинар на тему "Backend на современном C++".
Также делимся традиционным переводом полезного материала.
Одним из наиболее важных нововведений C++20 являются корутины. Корутина — это функция, которая может быть приостановлена ​​и после этого возобновлена. Функция становится корутиной, если она используете что-либо из следующего:
  • оператор co_await, чтобы приостановить выполнение до его возобновления
  • ключевое слово co_return, чтобы завершить выполнение и вернуть значение (опционально)
  • ключевое слово co_yield, чтобы приостановить выполнение и вернуть значение
Вдобавок тип возвращаемого значения корутины должен удовлетворять определенным условиям. Однако стандарт C++20 определяет только фреймворк для выполнения корутин, но не определяет никаких типов корутин, удовлетворяющих изложенным требованиям. Это означает, что нам нужно либо писать свои собственные, либо полагаться на сторонние библиотеки. В этой статье я покажу, как написать несколько простых примеров с использованием библиотеки cppcoro.Библиотека cppcoro содержит абстракции для корутин C++20, включая задачу (task), генератор (generator) и asyncgenerator. Задача представляет собой асинхронное вычисление, которое выполняется лениво (то есть только тогда, когда корутина ожидается (awaited)), а генератор — это последовательность значений некоторого типа T, которые также создаются лениво (то есть когда вызывается функция begin(), чтобы получить итератор, или на итераторе вызывается оператор ++).Давайте посмотрим на пример. Приведенная ниже функция produitems() является корутиной, потому что она использует ключевое слово co_yield для возврата значения и имеет тип возвращаемого значения cppcoro::generator<std::string>, который удовлетворяет требованиям к корутине-генератору.
#include <cppcoro/generator.hpp>
cppcoro::generator<std::string> produce_items()
{
  while (true)
  {
     auto v = rand();
     using namespace std::string_literals;
     auto i = "item "s + std::to_string(v);
     print_time();
     std::cout << "produced " << i << '\n';
     co_yield i;
  }
}
ПРИМЕЧАНИЕ: функция rand() используется исключительно для простоты примера. Не используйте эту устаревшую функцию в реальном продакшн коде.
Эта функция реализует бесконечный цикл, выполнение которого приостанавливается всякий раз, когда мы достигаем оператора co_yield. Эта функция выдает случайное число при каждом возобновлении выполнения, что происходит при итерировании генератора. Пример показан ниже:
#include <cppcoro/task.hpp>
cppcoro::task<> consume_items(int const n)
{
  int i = 1;
  for(auto const& s : produce_items())
  {
     print_time();
     std::cout << "consumed " << s << '\n';
     if (++i > n) break;
  }
  co_return;
}
Функция consume_items также является корутиной. Она использует ключевое слово co_return для завершения выполнения, а ее возвращаемый тип — cppcodo::task<>, который также удовлетворяет требованиям для типа корутины. Эта функция запускает цикл n раз, используя for с диапазоном. Этот цикл вызывает функцию begin() класса cppcoro::generator<std::string>, чтобы получить итератор, который впоследствии инкрементируется с помощью оператора ++. produceitems() возобновляется при каждом из этих вызовов и возвращает новое (случайное) значение. Если возникает исключение, оно перебрасывается функции, вызывающей begin() или оператор ++. В produceitems() функция может быть возобновлена бесконечное количество раз, хотя потребляющий код нуждается лишь в конечном числе вызовов.consume_items() может быть вызвана из main(). Однако, поскольку main() не может быть корутиной, она не может использовать оператор co_await для ожидания завершения ее выполнения. Чтобы помочь с этим, библиотека cppcoro предоставляет функцию с именем syncwait(), которая синхронно ожидает завершения указанной awaitable (которая ожидается в текущем потоке внутри вновь созданной корутины). Эта функция блокирует текущий поток до завершения операции и возвращает результат выражения coawait. В случае возникновения исключения оно перебрасывается вызывающей стороне.Следующий фрагмент кода показывает, как мы можем вызывать и ожидать require_items() из main():
#include <cppcoro/sync_wait.hpp>
int main()
{
   cppcoro::sync_wait(consume_items(5));
}
Вывод этой программы выглядит следующим образом:
cppcoro::generator<T> генерирует значения ленивым, но синхронным образом. Это означает, что использование оператора co_await из корутины, возвращающей этот тип, невозможно. Однако в библиотеке cppcoro есть асинхронный генератор cppcoro::asyncgenerator<T>, который делает это возможным.Мы можем изменить предыдущий пример следующим образом: возвращать значение, для вычисления которого требуется некоторое время, будет новая корутина next_value(). Мы симулируем такое поведение, ожидая случайное количество секунд. В каждой итерации цикла корутина produce_items() будет ожидать новое значение, а затем возвращать новый элемент на основе этого значения. Тип возврата на этот раз — cppcoro::async_generator<T>.
#include <cppcoro/async_generator.hpp>
cppcoro::task<int> next_value()
{
  using namespace std::chrono_literals;
  co_await std::chrono::seconds(1 + rand() % 5);
  co_return rand();
}
cppcoro::async_generator<std::string> produce_items()
{
  while (true)
  {
     auto v = co_await next_value();
     using namespace std::string_literals;
     auto i = "item "s + std::to_string(v);
     print_time();
     std::cout << "produced " << i << '\n';
     co_yield i;
  }
}
Потребитель требует небольшого изменения, потому что он должен ожидать каждого нового значения. Это реализуется с помощью оператора co_await в цикле for следующим образом:
cppcoro::task<> consume_items(int const n)
{
  int i = 1;
  for co_await(auto const& s : produce_items())
  {
     print_time();
     std::cout << "consumed " << s << '\n';
     if (++i > n) break;
  }
}
Оператор co_return больше не присутствует в этой реализации, хотя его можно добавить. Поскольку co_await используется в цикле for, функция является корутиной. Вам не нужно добавлять пустые операторы co_return в конец корутины, возвращающей cppcoro::task<>, точно так же, как вам не нужны пустые операторы return в конце обычной функции, возвращающей void. Предыдущая реализация требовала этого оператора, потому что не было вызова co_await, следовательно, co_return был необходим, чтобы сделать функцию корутиной.Никаких изменений в main() не требуется. Однако, когда мы в этот раз выполняем код, каждое значение генерируется через некоторый случайный интервал времени, как показано на следующем изображении:
Для полноты картины, функция print_time(), упомянутая в этих примерах, выглядит следующим образом:
void print_time()
{
   auto now = std::chrono::system_clock::now();
   std::time_t time = std::chrono::system_clock::to_time_t(now);
   char mbstr[100];
   if (std::strftime(mbstr, sizeof(mbstr), "[%H:%M:%S] ", std::localtime(&time)))
   {
      std::cout << mbstr;
   }
}
Еще одна важная деталь, на которую следует обратить внимание, — это то, что вызов co_await с заданной продолжительностью времени по умолчанию невозможен. Однако это стало возможным благодаря перегрузке оператора co_await. В Windows работает следующая реализация:
#include <windows.h>
auto operator co_await(std::chrono::system_clock::duration duration)
{
   class awaiter
   {
      static
         void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE,
            void* Context,
            PTP_TIMER)
      {
         stdco::coroutine_handle<>::from_address(Context).resume();
      }
      PTP_TIMER timer = nullptr;
      std::chrono::system_clock::duration duration;
   public:
      explicit awaiter(std::chrono::system_clock::duration d)
         : duration(d)
      {}
      ~awaiter()
      {
         if (timer) CloseThreadpoolTimer(timer);
      }
      bool await_ready() const
      {
         return duration.count() <= 0;
      }
      bool await_suspend(stdco::coroutine_handle<> resume_cb)
      {
         int64_t relative_count = -duration.count();
         timer = CreateThreadpoolTimer(TimerCallback,
            resume_cb.address(),
            nullptr);
         bool success = timer != nullptr;
         SetThreadpoolTimer(timer, (PFILETIME)&relative_count, 0, 0);
         return success;
      }
      void await_resume() {}
   };
   return awaiter{ duration };
}
Эта реализация взята из статьи Coroutines in Visual Studio 2015 - Update 1.АПДЕЙТ: код был изменен на основе отзывов. Смотрите комментарии.Чтобы узнать больше о корутинах, смотрите:
ЗАБРАТЬ СКИДКУ
===========
Источник:
habr.com
===========

===========
Автор оригинала: Marius Bancila
===========
Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_c++, #_si++ (си++), #_korutiny (корутины), #_bekend (бэкенд), #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
)
, #_programmirovanie (
Программирование
)
, #_c++
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:20
Часовой пояс: UTC + 5