Недавно техническая спецификация сопрограмм, Coroutines TS, дошла до состояния "опубликована". Сопрограммы реализованы в MS VC++ 2017.
Что это такое и как их писать?
В С++ сопрограммой называется функция, в которой используются
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;
}
При этом, хотя тело сопрограммы изменяется, она остается функцией.
Компилятор добавляет неявную переменную с типом 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();
В сопрограмме нельзя использовать return y;, вместо него используется co_return y;,
который заменяется на
p.return_value(y);
goto final_suspend;
Если сопрограмма не предусматривает возврат значения по завершении, то используется co_return; (без выражения) и соответствующая ему функция p.return_void();.
При этом не обязательно писать co_return; в конце сопрограммы.
Приостановка сопрограммы происходит в операторе 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<bool> is_ready;
};
// Тип результата f
struct Awaiter {
std::shared_ptr<SharedState> 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<SharedState>();
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.
Минимальный тип возвращаемого значения сопрограммы выглядит так:
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*> y_ptr = nullptr;
};
std::shared_ptr<promise_type> promise;
static void Deleter(promise_type* p) {
auto h = std::experimental::coroutine_handle<P>::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.
Для выражения a в co_await a могут применяться дополнительные преобразования:
p.await_transform(a) валидно, то a заменяется на p.await_transform(a);a есть оператор operator co_await, то a заменяется на operator co_await(a);Таким образом возможен вариант
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 по умолчанию.
Компилятор использует класс coroutine_traits для получения promise_type.
Для сопрограммы Future coro(A1 a1, A2 a2) будет использован тип std::experimental::coroutine_traits<Future, A1, A2>::promise_type.
Реализация по-умолчанию выдает Future::promise_type, однако это может быть переопределено пользователем.
co_yield e; эквивалентен co_await p.yield_value(e); и используется в генераторах - специальных сопрограммах которые предназначены для выдачи последовательности значений.
Продвижение своими сайтами как стратегия роста и независимости