В преддверии старта курса "C++ Developer. Professional" приглашаем записаться на открытый вебинар на тему "Backend на современном C++".
Также делимся традиционным переводом полезного материала.
Одним из наиболее важных нововведений C++20 являются корутины. Корутина — это функция, которая может быть приостановлена и после этого возобновлена. Функция становится корутиной, если она используете что-либо из следующего:
оператор
co_await
, чтобы приостановить выполнение до его возобновления
ключевое слово
co_return
, чтобы завершить выполнение и вернуть значение (опционально)
ключевое слово
co_yield
, чтобы приостановить выполнение и вернуть значение
Вдобавок тип возвращаемого значения корутины должен удовлетворять определенным условиям. Однако стандарт C++20 определяет только фреймворк для выполнения корутин, но не определяет никаких типов корутин, удовлетворяющих изложенным требованиям. Это означает, что нам нужно либо писать свои собственные, либо полагаться на сторонние библиотеки. В этой статье я покажу, как написать несколько простых примеров с использованием библиотеки cppcoro.
Библиотека cppcoro
содержит абстракции для корутин C++20, включая задачу (task
), генератор (generator
) и async_generator
. Задача представляет собой асинхронное вычисление, которое выполняется лениво (то есть только тогда, когда корутина ожидается (awaited)), а генератор — это последовательность значений некоторого типа T, которые также создаются лениво (то есть когда вызывается функция begin()
, чтобы получить итератор, или на итераторе вызывается оператор ++
).
Давайте посмотрим на пример. Приведенная ниже функция produce_items()
является корутиной, потому что она использует ключевое слово 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>
, чтобы получить итератор, который впоследствии инкрементируется с помощью оператора ++
. produce_items()
возобновляется при каждом из этих вызовов и возвращает новое (случайное) значение. Если возникает исключение, оно перебрасывается функции, вызывающей begin()
или оператор ++
. В produce_items()
функция может быть возобновлена бесконечное количество раз, хотя потребляющий код нуждается лишь в конечном числе вызовов.
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::async_generator<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.
АПДЕЙТ: код был изменен на основе отзывов. Смотрите комментарии.
Чтобы узнать больше о корутинах, смотрите:
Узнать подробнее о курсе "C++ Developer. Professional".
Записаться на открытый вебинар на тему "Backend на современном C++".