Страницы

Поиск по вопросам

среда, 27 ноября 2019 г.

Сопрограммы в С++

#c++ #сопрограмма


Недавно техническая спецификация сопрограмм, Coroutines TS, дошла до состояния "опубликована".
Сопрограммы реализованы в MS VC++ 2017.
Что это такое и как их писать?

(Upd: текущая версия Coroutines TS - n4775 от 2018-10-07)
    


Ответы

Ответ 1



В С++ сопрограммой называется функция, в которой используются co_await, co_yield, co_return. Future coroutine(X x) { Y y = co_await f(x); co_return y; } Компилятор переписывает тело сопрограммы, превращая ее в машину состояний. Под данные сопрограммы выделяется память при помощи оператора new. Future coroutine(X x) { struct CoroutineState { Future::promise_type p; X x; Y y; int state = 0; void run() { switch (state) { case 0: ... state = 1; // приостановка return; case 1: // точка возобновления ... }; } }; auto* s = new CoroutineState; auto result = s->p.get_return_object(); s->x = x; s->run(); return result; } При этом, хотя тело сопрограммы изменяется, она остается функцией. promise_type (ч.1) Компилятор добавляет неявную переменную с типом Future::promise_type. Эта переменная используется для создания результата функции-сопрограммы (p.get_return_object), обработки исключений, реализации co_return. Также компилятор добавляет точки приостановки в начале и конце сопрограммы, оборачивая тело сопрограммы в следующий код: Future::promise_type p; co_await p.initial_suspend(); try { // тело сопрограммы Y y = co_await f(x); co_return y; // конец тела сопрограммы } catch(...) { p.unhandled_exception(); } final_suspend: co_await p.final_suspend(); co_return В сопрограмме нельзя использовать return y;, вместо него используется co_return y;, который заменяется на p.return_value(y); goto final_suspend; Если сопрограмма не предусматривает возврат значения по завершении, то используется co_return; (без выражения) и соответствующая ему функция p.return_void();. При этом не обязательно писать co_return; в конце сопрограммы. co_await Приостановка сопрограммы происходит в операторе co_await. Код Y y = co_await f(x); заменяется на auto e = f(x); if (!e.await_ready()) { ... приостановка ... std::experimental::coroutine_handle<> h = ...; if (e.await_suspend(h)) return; resume: // точка возобновления для h.resume() ... возобновление ... } Y y = e.await_resume(); Стандартная библиотека предоставляет класс coroutine_handle, который позволяет возобновить приостановленную сопрограмму. Функция f принимает его через e.await_suspend(h). Когда значение y будет вычислено, она должна вызвать h.resume(), и вернуть вычисленное значение через e.await_resume(). Для stackless сопрограммы, функия f может быть написана следующим образом: // Общие данные фонового потока и Awaiter. struct SharedState { std::experimental::coroutine_handle<> h; Y value; std::atomic is_ready; }; // Тип результата f struct Awaiter { std::shared_ptr s; bool await_ready() { return false; } bool await_suspend(std::experimental::coroutine_handle<> h) { s->h = h; return !s->is_ready.exchange(true); // True если фоновый поток уже завершился // и у нас есть s->value } Y await_resume() { return s->value; } }; Awaiter f(X x) { auto s = std::make_shared(); std::thread([=]{ // Запуск фонового потока для вычислений s->value = ...; if (s->is_ready.exchange(true)) { // True, если await_suspend уже была вызвана // и у нас есть s->h s->h.resume(); } }).detach(); return Awaiter{s}; } Для stackful сопрограммы (например Fibers в Windows), await_suspend должна сама замораживать поток (SwitchToFiber). Точка возобновления будет внутри await_suspend, поэтому она должна возвращать false. promise_type (ч.2) Минимальный тип возвращаемого значения сопрограммы выглядит так: struct Future { struct promise_type { Future get_return_object() { return {this}; } std::experimental::suspend_never initial_suspend() { return {}; } std::experimental::suspend_always final_suspend() { return {}; } void return_value(Y& y) { y_ptr = &y; } std::atomic y_ptr = nullptr; }; std::shared_ptr promise; static void Deleter(promise_type* p) { auto h = std::experimental::coroutine_handle

::from_promise(*p); h.destroy(); // удаляет CoroutineState } Future(promise_type* p) : promise(p, Deleter) {} Y BlockingGet() { while (promise->y_ptr == nullptr) Sleep(1); // ждем return *promise->y_ptr; // дождались вызова p.return_value(y) } }; Future::promise_type должен иметь get_return_object, initial_suspend, final_suspend и либо return_void либо return_value. Для реализации initial_suspend и final_suspend можно использовать стандартные suspend_never и suspend_always, которые возвращают в await_ready значения true и false соответственно. Такая сопрограмма будет всегда засыпать в конце. От самого Future требуется только чтобы он удалил сопрограмму через h.destroy(). Future может (но не обязан) повторять интерфейс Awaiter, чтобы быть совместимым с co_await. await_transform и operator co_await Для выражения a в co_await a могут применяться дополнительные преобразования: если выражение p.await_transform(a) валидно, то a заменяется на p.await_transform(a); если для типа a есть оператор operator co_await, то a заменяется на operator co_await(a); если в результате получилось prvalue, то оно копируется во временную переменную, иначе используется как есть. Таким образом возможен вариант auto& e = operator co_await(p.await_transform(f(x))); if (!e.await_ready()) { ... } Например можно определить operator co_await(std::chrono::duration) и писать co_await 10ms;. Выделение памяти Объект CoroutineState создается при помощи new. Однако, если есть функция p.get_return_object_on_allocation_failure(), то будет сгенерирован следующий код: auto* s = new(std::nothrow) CoroutineState; if (!s) { return p.get_return_object_on_allocation_failure(); } auto result = s->p.get_return_object(); Это позволяет обрабатывать ошибки выделения памяти. Также, аргументы сопрограммы могут участвовать в выделении памяти. Для сопрограммы Future coro(A1 a1, A2 a2), если есть функция operator new(std::size_t, A1, A2), то она будет вызвана вместо оператора new по умолчанию. promise_type (ч.3), coroutine_traits Компилятор использует класс coroutine_traits для получения promise_type. Для сопрограммы Future coro(A1 a1, A2 a2) будет использован тип std::experimental::coroutine_traits::promise_type. Реализация по-умолчанию выдает Future::promise_type, однако это может быть переопределено пользователем. co_yield co_yield e; эквивалентен co_await p.yield_value(e); и используется в генераторах - специальных сопрограммах которые предназначены для выдачи последовательности значений. Примеры Как использовать сопрограммы С++ с Boost.Asio?

Комментариев нет:

Отправить комментарий