Недавно техническая спецификация сопрограмм, 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);
и используется в генераторах - специальных сопрограммах которые предназначены для выдачи последовательности значений.
Виртуальный выделенный сервер (VDS) становится отличным выбором
GetModuleFileNameEx частенько возвращает 0GetLastError говорит об ошибке 299 (ERROR_PARTIAL_COPY - only part of a ReadProcessMemory or WriteProcessMemory request was completed)
Как произвести ретрансляцию трафика через сокеты в Boost AsioПытаюсь реализовать что то вроде сервера, к которому подключаются клиенты с одной...
Здравствуйте! Появилась задача - сделать нейросеть, которая будет распознавать числа от 1 до 15 на С++ QtНикогда раньше не имел с этим дело