Прим. Wunder Fund: В статьи описаны базовые подходы к работе с корутинами в 20м стандарте С++, на паре практических примеров разбораны шаблоны классов для промисов и фьючеров. По нашему скромному мнению, можно было бы реализовать и поизящнее. Приходите к нам работать, если имеете сильные мнения о корутинах хе-хе.
Возникает такое ощущение, что тема реализации корутин в C++20 окутана серьёзной неопределённостью. Полагаю, это так из-за того, что в проекте технической спецификации C++20 сказано, что работа над механизмами корутин всё ещё ведётся, в результате в данный момент нельзя ожидать полной поддержки этих механизмов компиляторами и стандартной библиотекой. Множество проблем, вероятно, возникает из-за отсутствия официальной документации по работе с корутинами. Нам дали синтаксическую поддержку корутин в C++ (co_yield
и co_return
), но не всё то, что я счёл бы признаками их полной библиотечной поддержки. В стандартной библиотеке имеются хуки и базовый функционал поддержки корутин, но нам приходится самостоятельно встраивать всё это в наши собственные классы. Я ожидаю, что полная поддержка корутин-генераторов появится в C++23.
Спецификация C++20, очевидно, направлена на поддержку параллельных (или асинхронных) корутин с использованием co_await
, что усложняет реализацию более простых синхронных корутин-генераторов. Среди требований к реализации наших корутин имеются сведения об использовании Future
и Promise
, что похоже на то, как при реализации асинхронных потоков используется std::async.
Если вы — Python- или C#-разработчик и ожидаете увидеть в C++ простую механику работы с корутинами, то вас ждёт разочарование, так как фреймворк общего назначения C++20 недоработан. Учитывая это, можно отметить, что в интернете имеется множество публикаций, в состав кода, обсуждаемого в которых, входит шаблонный класс, поддерживающий корутины-генераторы. В этом материале вы найдёте шаблон корутины, применимый на практике, а также примеры кода. Всё это предваряется общими сведениями о корутинах.
Что такое корутины?
Я впервые столкнулся с корутинами, увидев инструкцию yield
в CLU. Корутины, наподобие генераторов в Python (и конструкции yield return
в C#), определяются с использованием функционального синтаксиса, а доступ к ним организуется с применением синтаксических конструкций цикла for
. Корутины описывались как взаимодействующие программы (но не как конкурентные программы), выполняющиеся в одном потоке. Существуют и другие разновидности корутин. Для того чтобы разобраться в том, чем отличаются друг от друга функции, генераторы и потоки, можно начать с этой статьи из Википедии.
В этом материале я уделяю основное внимание корутинам, выполняемым в контексте вызывающей стороны, применение которых позволяет двум различным блокам кода периодически передавать друг другу управление ходом выполнения программы.
Новая инструкция co_yield
в C++20 позволяет одной программе предоставить фрагмент данных, и в то же время вернуть управление ходом выполнения программы вызывающей программе для обработки этих данных. В общем-то, это — всего лишь витиеватый способ сказать о том, что корутины в C++20 дают нам однопоточную реализацию паттерна продюсер/консьюмер (producer/consumer).
Мы можем показать классический пример взаимодействия продюсера и консьюмера, связанный с применением корутин, подготовив следующую UML-диаграмму последовательности.
Блоки, олицетворяющие время, когда управление ходом выполнения программы находятся у той или иной сущности, показывают передачу управления от одной программе другой.
Когда управление ходом выполнения программы передаётся от одной программе другой — текущее состояние программы должно быть сохранено, а затем, когда выполнение программы возобновляется, должно быть восстановлено. В случае с консьюмером это происходит в рамках обычного механизма вызова функции, когда текущий кадр стека сохраняет состояние программы. В случае с продюсером (это — корутина) нужна дополнительная поддержка со стороны компилятора и системы выполнения кода. А именно, при возвращении консьюмеру некоего значения нужно сохранять кадр стека продюсера.
В спецификации C++20 сказано, что состояние корутины сохраняется в куче, то есть — корутины не подходят для встраиваемых систем, которые не используют динамическую память. Но в спецификации абсолютно чётко заявлено, что в конкретной реализации языка использование кучи может быть убрано при соблюдении следующих условий:
-
Если время жизни корутины строго ограничено временем жизни вызывающей стороны.
-
Если размер состояния корутины может быть определён во время компиляции кода.
На практике, в случае с простыми корутинами-генераторами, которым посвящён этот материал, подобные корутины соответствуют этим критериям, состояние таких корутин может быть сохранено в кадре стека вызывающей стороны. Исследование использования кучи в двух примерах, рассмотренных в этой статье, показывает, что и GCC-11, и Clang-12 (последние версии компиляторов на момент написания материала) используют для сохранения состояния корутин кучу. Учитывая то, что поддержка корутин в компиляторах появилась сравнительно недавно, и то, что она сейчас находится в процессе развития, вполне возможно, что в более поздних их версиях соответствующий код будет оптимизирован, или, возможно, появится поддержка опций компиляторов для включения или отключения сохранения состояния корутин в динамической памяти.
Для организации поддержки сохранения и восстановления состояния корутин мы должны предоставить системе вспомогательный класс, который интегрируется с механизмами поддержки корутин в стандартной библиотеке, описанными в заголовочном файле, подключаемом к коду с помощью конструкции #include <coroutine>
. Именно в этой сфере сейчас и находится всё то, что вызывает сложности в реализации корутин.
Поддержка корутин в C++20
Для того чтобы приступить к разговору о корутинах — мы можем создать одну из них, выдающую фразу «Hello world!» в виде трёх отдельных объектов. Её код показан ниже (тут нам нужно подключить заголовочный файл <coroutine>
).
#include <coroutine>
X coroutine()
{
co_yield "Hello ";
co_yield "world";
co_return "!";
}
Первое, на что тут можно обратить внимание, заключается в том, что это — не определение функции! Мы только что использовали синтаксис функции для определения блока кода, которому могут быть переданы аргументы при создании его экземпляра. У функции имелась бы инструкция return
(или как в случае с void-функциями, подразумевалось бы, что значение явным образом не возвращается). А тут код выдаёт три отдельных значения. Обратите внимание на то, что воспользоваться инструкцией return
в корутине нельзя.
Второй интересный момент заключается в том, что мы возвращаем какой-то неизвестный (на данный момент) объект типа X
. Это — объект, который реализует корутину. Компилятор реорганизует наш блок кода для реализации механизмов корутин, предусматривающих сохранение и восстановление состояния, но сейчас ему нужна небольшая помощь от нас, которая выражается в написании вспомогательного класса X
.
В блоке кода, представляющего корутину, мы используем co_yield
для выдачи значения и сохранения состояния корутины, а co_return
— для выдачи значения и выхода из корутины без сохранения её состояния.
Это предельно простой пример использования корутины, где обратиться к ней нужно в точности три раза — как показано в следующем примере:
auto x = coroutine();
std::cout << x.next();
std::cout << x.next();
std::cout << x.next();
std::cout << std::endl;
После того как мы потребили все значения, выданные корутиной, её работа завершается, она освобождает всю память, использованную для хранения её состояния.
В нашем примере у объекта корутины есть метод next
, вызов которого приводит к выполнению следующих действий:
-
Приостановка выполнения текущего кода консьюмера.
-
Восстановление состояния корутины (продюсера).
-
Возобновление выполнения кода корутины с предыдущей инструкции, выдающей значение (или с начала блока кода).
-
Сохранение значения, полученного из следующей инструкции, выдающей значение.
-
Сохранение состояния корутины.
-
Восстановление состояния консьюмера.
-
Возобновление выполнения кода консьюмера путём передачи ему значения, сохранённого после выполнения соответствующей инструкции, выдающей значение.
Сейчас в стандартной библиотеке нет шаблона для нашего класса X
. В результате нам нужно разобраться с тем, что сейчас имеется в стандартной библиотеке в плане поддержки корутин. Пример шаблонного класса показан ниже, там, где мы будем говорить о практическом применении корутин, а пока мы взглянем на базовый пример, код которого написан исключительно в учебных целях.
Для того чтобы написать собственный вспомогательный класс X
для корутин, нам нужно обеспечить поддержку операций жизненного цикла корутины, предоставив реализации особых методов. Стандарт C++20 определяет требования к этим методам, используя концепции, ознакомиться с которыми можно здесь и здесь. Для того чтобы тут мы не отвлекались от темы корутин, мы применим традиционный для C++ поход к описанию методов, наличие которых ожидается системой, в виде частей нашего класса.
Для использования корутин в C++20 нам нужно подготовить два взаимосвязанных вспомогательных класса:
-
Класс для сохранения состояния корутины и для сохранения выданных данных. Обычно его называют
promise
. -
Класс для управления объектом корутины (
promise
). Это классX
, который, по традиции, называютfuture
.
В объекте типа Promise
нужно реализовать несколько методов жизненного цикла корутины. Пока мы сосредоточимся на поддержке выражений, выдающих значения, и не будем обращать внимания на методы, необходимые для управления состоянием корутины.
Так как наша корутина использует инструкцию co_yield
со значением const char*
, нам нужен метод со следующей сигнатурой:
std::suspend_always yield_value(const char* value);
Аргумент — это выдаваемый корутиной объект, возвращаемый тип сообщает системе выполнения кода о том, нужно ли сохранять состояние потока, что, в случае с однопоточной корутиной, мы всегда планируем делать, возвращая объект std::suspend_always
. В данной ситуации есть возможность возврата объекта std::suspend_never
, что допустимо при работе с асинхронными корутинами, но это ведёт к множеству сложностей, связанных с управлением приостановленными потоками и с возобновлением их работы. Мы не собираемся с этим связываться, работая над нашей простой синхронной корутиной.
Метод yield_value
обязан сохранить свой аргумент, в результате он может быть возвращён вызывающей программе (консьюмеру). Вот как выглядит типичная реализация этого метода:
std::suspend_always yield_value(const char* value) {
this->value = value;
return {};
}
Если вы ещё не сталкивались с современной синтаксической конструкцией C++ return {}
, то знайте, что её смысл заключается всего лишь в том, чтобы создать объект возвращаемого типа этого метода, конструируемый по умолчанию. Ещё тут можно было использовать return
std::suspend_always{}
.
Для поддержки инструкции co_return
, которая выдаёт значение, но не сохраняет состояние, нам нужен второй метод жизненного цикла корутины:
void return_value(const char* value) {
this->value = std::move(value);
}
Вызов co_return
завершает выполнение корутины, соответствующая функция жизненного цикла корутины имеет возвращаемый тип void
, так как состояние корутины будет уничтожено.
Если не вдаваться в детали реализации класса X
, можно показать, как компилятор может расширить код сущности-консьюмера, превратив его в набор встроенных последовательных операций, а после этого взглянуть на методы жизненного цикла. В следующем примере метод promise
даёт доступ к объекту Promise
, который сохраняет состояние корутины и выдаваемое ей значение. Метод next
может получить сохранённое значение из объекта Promise
:
auto x = coroutine();
x.promise().yield_value("Hello "); // сохраняется значение и состояние
std::cout << x.next();
x.promise().yield_value("world"); // сохраняется значение и состояние
std::cout << x.next();
x.promise().return_value("!"); // сохраняется значение, но не состояние
std::cout << x.next();
std::cout << std::endl;
Можно видеть, что компилятор преобразовал наш код, превратив два блока кода в один, представленный последовательным набором вызовов чередующихся методов.
Перед тем как мы разберём полноценный пример, в котором имеется весь необходимый шаблонный код для классов Promise
и Future
, нам нужно взглянуть на альтернативный способ написания кода корутин-генераторов:
X coroutine()
{
co_yield "Hello ";
co_yield "world";
co_yield "!";
// подразумеваемый вызов co_return;
}
При применении такого подхода мы используем co_yield
для всех значений, не выделяя особым образом (с помощью co_return
) последнюю операцию выдачи значения; мы просто позволяем блоку кода завершить работу. Компилятор сам добавит в нужное место инструкцию co_return
для завершения работы корутины.
Для обработки инструкции co_return
(без значения) нам нужна реализация особого метода жизненного цикла корутины void return_void
:
void return_void() {
this->value = nullptr;
}
Класс Promise
не может предоставить и метод return_value
, и метод return_void
, которые считаются взаимоисключающими.
В этом простейшем примере код консьюмера не меняется, так как он выполняет чтение в точности трёх значений. В более реалистичном примере, где чтение значений из корутин выполняется в цикле, нам нужно каким-то образом отметить конец потока данных. Тут используются указатели, в результате для завершения цикла может быть использован nullptr
; в противном случае наиболее общим подходом можно назвать объект std::optional
.
Наш новый консьюмер с возможностью остановки работы выглядит так:
auto x = coroutine();
while (const char* item = x.next()) {
std::cout << item;
}
std::cout << std::endl;
Мы могли бы описать цикл так:
while (auto item = x.next()) {
Но тут мы решили сохранить явное объявление типа, в результате из этого кода ясно то, как именно используется генератор.
Полная версия этого кода находится в файле char_demo.cpp
в GitHub-репозитории coroutines-blog.
Работа с корутинами
Корутины — это удобный механизм для реализации множества алгоритмов в виде отдельных блоков кода, а не в такой форме, когда соответствующий код собирают в одном месте, где реализации разных алгоритмов оказываются смешанными друг с другом.
В качестве примера рассмотрим встроенное устройство, которое наблюдает за некими значениями, например, за показателями температуры, и выводит эти значения на последовательный порт (RS232), снабжая их отметкой времени. Это может быть время с момента загрузки устройства, или точное время, синхронизированное по сети.
Отметка времени и значение сохраняются в виде числа с плавающей запятой (каждое занимает 4 байта) и в двоичном виде сохраняются в виде потока байтов. Сделано это ради снижения сложности кода и размера обрабатываемых данных. Поток данных выглядит примерно так, как показано ниже (тут используется обратный (little endian) порядок байтов).
В нашем приложении, которое занимается сбором данных, нужно считывать этот поток и помещать данные в структуру, содержащую два значения с плавающей запятой, после чего — выводить эти значения на соответствующем устройстве, вроде некоего дисплея. А если показатель температуры превышает заданное пороговое значение — сообщение нужно снабдить предупреждением.
Общий алгоритм работы системы будет выглядеть так:
-
Чтение 4 байтов, необходимых для создания отметки времени.
-
Чтение 4 байтов, необходимых для создания элемента данных.
-
Создание структуры данных, содержащих оба значения, представленных числами с плавающей запятой.
-
Вывод значений из структуры данных.
-
Вывод предупреждения в том случае, если показатель температуры превысит заданное пороговое значение.
Теперь подумаем о том, что произойдёт в том случае, если поток данных прервётся в ходе передачи значений, представленных числами с плавающей запятой. В нашем коде имеется средство для обработки ошибки, связанной с окончанием потока, применяемое для каждой отдельной операции чтения данных (речь идёт о восьми самостоятельных операциях по чтению одного байта). Даже при разумном использовании функций этот код будет представлять собой сложный набор условий и инструкций, направленных на реконструкцию данных. Такой код непросто, хотя и интересно, писать и поддерживать.
При использовании корутин подобный код можно разбить на два блока:
-
Парсинг данных.
-
Вывод данных и, возможно, предупреждающего сообщения.
Мы на практике пойдём ещё дальше и разобьём первый блок на две части:
-
Разбор необработанного потока байтов и преобразование их в числа с плавающей запятой.
-
Сохранение отметки времени и показателя температуры в структуре.
Шаблон Future для корутины
Первым шагом нашей работы будет создание шаблона для классов, о которых мы уже говорили, представляющих класс Future
корутины и класс Promise
для данных.
Класс Promise, хранящий данные
Вот — класс Promise
, представляющий собой структуру, вложенную в класс Future
:
template <typename T>
class Future
{
class Promise
{
public:
using value_type = std::optional<T>;
Promise() = default;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {
std::rethrow_exception(std::move(std::current_exception()));
}
std::suspend_always yield_value(T value) {
this->value = std::move(value);
return {};
}
void return_void() {
this->value = std::nullopt;
}
inline Future get_return_object()
value_type get_value() {
return value;
}
private:
value_type value{};
};
…
};
Структура Promise
(которую мы объявили приватной для включающего её в себя класса Future
) сохраняет отдельное значение данных в приватном объекте std::optional
с методом доступа get_value
. Используя объект std::optiona
l мы можем воспользоваться std::nullop
для проверки на завершение работы корутины после вызова метода return_void
. Мы придерживаемся стиля метапрограммирования шаблонов C++, определяя признак типа value_type
, что позволяет нам опрашивать класс для определения типа данных, лежащего в его основе.
Мы создаём конструктор, используемый по умолчанию и два метода жизненного цикла, необходимых для Promise-объекта корутины (initial_suspend
и final_suspend
), которые всегда приостанавливают работу корутины, чтобы мы могли бы работать в однопоточном режиме. Эти методы жизненного цикла необходимы, но это — лишь их стандартные реализации, которые не нуждаются в дальнейшем рассмотрении.
Ещё нам надо указать то, как фреймворк должен обрабатывать неперехваченные исключения. Вместо того чтобы отвлекаться на их обработку и создавать механизмы восстановления работы после возникновения ошибки, мы просто передаём исключение вызывающей стороне, которая должна принять решение о том, что с ними делать дальше.
Методы yield_value
и return_void
, о которых мы уже говорили, определены для того, чтобы копировать или перемещать выданное корутиной значение в хранилище std::optional
, или для того, чтобы использовать std::nullopt
для указания на завершение работы корутины. Обратите внимание на использование std::move
. Это сделано для того, чтобы обеспечить поддержку семантики перемещения данных для аргументов функции, передаваемых по значению: это необходимо, например, если надо выдать std::unique_ptr
.
Ещё один метод, который нужно подготовить, это — get_return_object
. Он должен возвращать объект Future
для данного объекта Promise
. Так как мы пока не завершили определение класса Future
, нам нужно реализовать этот метод после того, как будут готовы классы Future
и Promise
.
Класс Future — менеджер контекста корутины
Сам класс Future
предоставляет нам конструктор/деструктор для управления составным объектом Promise
, а так же — механизм для получения значений, выданных корутиной (метод next
, о котором мы уже говорили):
template <typename T>
class Future
{
struct Promise { … };
public:
using value_type = T;
using promise_type = Promise;
explicit Future(std::coroutine_handle<Promise> handle)
: handle (handle)
{}
~Future() {
if (handle) { handle.destroy(); }
}
// Promise::value_type next() { … }
private:
std::coroutine_handle<Promise> handle;
};
В стандартной библиотеке имеется поддержка управления объектами Promise
через шаблонный класс std::coroutine_handle
, передаваемый в виде аргумента конструктору класса Future
. Нам нужно сохранить этот объект coroutine_handle
и обеспечить вызов его метода destroy
при уничтожении объекта Future
.
Стандартная библиотека предъявляет ещё одно требование для класса Future
, в соответствии с которым мы должны определить вложенный тип promise_type
, что позволит шаблонам стандартной библиотеки выяснять тип данных, лежащий в основе класса.
using promise_type = Promise;
В нашей реализации метода next
необходимо обеспечить проверку того, что объект Promise
всё ещё актуален, или вернуть пустой объект std::optional
:
Promise::value_type next() {
if (handle) {
handle.resume();
return handle.promise().get_value();
}
else {
return {};
}
}
Вот что мы делаем для возврата значения, выданного корутиной:
-
Мы просто проверяем, существует ли всё ещё корутина (её объект
handle
не был уничтожен). -
Мы вызываем метод
resume
объектаcoroutine_handle
для выполнения кода до следующей инструкцииco_yield
. -
Мы возвращаем значение, сохранённое методом
yield_value
объектаPromise
: благодаря поддержке стандартной библиотеки будут обработаны операции восстановления и сохранения состояния корутины. -
Если корутин была уничтожена — мы возвращаем пустое значение (
std::nullopt
).
Теперь, когда определён класс Future
, мы можем дополнить объект Promise
необходимым методом get_return_object
:
template <typename T>
inline Future<T> Future<T>::Promise::get_return_object()
{
return Future{ std::coroutine_handle<Promise>::from_promise(*this) };
}
Тут мы используем метод std::from_promise для создания объекта coroutine_handle
, который передаётся конструктору Future
.
Как видите, перед нами — всего лишь стандартный код, заготовка для создания классов Future
и Promise
.
Теперь, когда завершены подготовительные операции, этот шаблон может быть использован в применении к большинству типов данных и классов. Я сказал бы, что он применим ко всем классам, но всегда найдутся особые случаи, в которых воспользоваться им не получится.
Корутина, занимающаяся сбором данных
Теперь можно сосредоточиться на нашей реальной задаче, которая заключается в организации сбора данных. Первый шаг работы заключается в написании корутины, занимающейся чтением данных из объекта istream
и выдачей значений, представляющих собой числа с плавающей запятой:
Future<float> read_stream(std::istream& in)
{
int count{};
uint8_t byte;
while (in >> byte) {
data = data << 8 | byte;
if (++count == 4) {
co_yield reinterpret_cast<float>(&data);
data = 0;
count = 0;
}
}
}
Тут мы просто читаем блоки данных размером 4 байта и помещаем их в 32-битные слова, после чего используем приведение типов, интерпретируя соответствующую область памяти в виде числа с плавающей запятой для инструкции co_yield
. Если поток данных завершается в ходе чтения 4-байтового слова, мы игнорируем частично прочитанное значение и завершаем работу корутины.
Мы можем подтвердить работоспособность этой корутины, просто выводя каждое float-значение, которое прочитано из стандартного потока ввода:
auto raw_data = read_stream(std::cin);
while (auto next = raw_data.next()) {
std::cout << *next << std::endl;
}
А, так как надо сохранить пары значений в структуре данных, можно воспользоваться второй корутиной, которая инкапсулирует соответствующий алгоритм:
struct DataPoint
{
float timestamp;
float data;
};
Future<DataPoint> read_data(std::istream& in)
{
std::optional<float> first{};
auto raw_data = read_stream(in);
while (auto next = raw_data.next()) {
if (first) {
co_yield DataPoint{*first, *next};
first = std::nullopt;
}
else {
first = next;
}
}
}
И, опять же, если входной поток неожиданно завершается при чтении данных, относящихся к отметке времени или к элементу данных — мы отбрасываем неполные данные.
Последний шаг этого примера заключается в обработке значений, представляющих отметку времени и данные:
static constexpr float threshold{25.0};
int main()
{
std::cout << std::fixed << std::setprecision(2);
std::cout << "Time (ms) Data" << std::endl;
auto values = read_data(std::cin);
while (auto n = values.next()) {
std::cout << std::setw(8) << n->timestamp
<< std::setw(8) << n->data
<< (n->data > threshold ? " Threshold exceeded" : "")
<< std::endl;
}
return 0;
}
Этот код выражает то, как мы решили обрабатывать данные, а все тонкости преобразования байтов в значения с плавающей запятой и их записи в структуру данных, скрыты в соответствующей корутине.
Хочется надеяться, что теперь вы способны прочувствовать плюсы использования корутин для разделения различных аспектов реализации сложных алгоритмов на более простые блоки кода. Сейчас, пользуясь C++20, нужно выполнять массу действий, которые кажутся сложными или ненужными для достижения нашей цели и заключаются в создании классов Future
и Promise
. Но я тем не менее надеюсь, что в C++23 уже будет встроено нечто подобное этому шаблону, что позволит программистам уделять внимание написанию собственного кода, не отвлекаясь на создание вспомогательных механизмов.
Протестировать этот код можно, воспользовавшись простым Python-скриптом, например, таким, который показан ниже. Он, в частности, выдаёт четыре жёстко заданных в коде элемента данных:
import struct
import sys
start = 0.0
for ms, value in enumerate([20.1, 20.9, 20.8, 21.1]):
sys.stdout.buffer.write(struct.pack('>ff', start + ms*0.1, value))
Если скомпилированный исполняемый файл называется datapoint_demo
, то мы можем воспользоваться следующим конвейером в командной строке Linux и убедиться в работоспособности корутин:
# Linux
python3 test_temp.py | ./datapoint_demo
В результате будет выведено следующее:
Time (ms) Data
0.00 20.10
0.10 20.90
0.20 20.80
0.30 21.10 Threshold exceeded
Полный вариант кода этого примера, представленный файлами future.h
и datapoint_demo.cpp
, можно найти здесь. Для того чтобы скомпилировать эти примеры с использованием GCC (версии 10 или выше), нужно воспользоваться -std=c++20
и -fcoroutines
в командной строке g++
.
В следующем материале я планирую добавить в шаблонный класс Future
поддержку итераторов, что позволит использовать корутину в цикле for
или в виде входного итератора для неких библиотечных механизмов.
Итоги
Корутины — это мощная техника программирования, позволяющая разделять различные аспекты реализаций сложных алгоритмов, описывая их в виде самостоятельных и достаточно простых блоков кода.
C++20, как Python и C#, использует функциональный синтаксис для определения кода корутин. Многие программисты поначалу находят это странным, так как это — всего лишь синтаксическая конструкция для описания инструкций, входящих в состав корутины.
Как мы видели, то, что сейчас нет простого стандартного шаблона для создания корутин-генераторов, усложняет жизнь тем, кто только собирается попробовать корутины. Сейчас применение корутин несколько напоминает сборку пазла, картинки-загадки, в условиях, когда тот, кто пазл собирает, не видел картинки, которая у него должна получиться. Поначалу такая задача выглядит пугающе сложной, но решить её вполне реально. Надеюсь, что шаблон Future
, который мы тут рассмотрели, это и есть та картинка, глядя на которую вы сможете собирать собственные пазлы.
О, а приходите к нам работать? 😏
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Присоединяйтесь к нашей команде.
Автор:
mr-pickles