Recently, a coroutine technical specification, Coroutines TS , has reached the "published" state. Coroutines are implemented in MS VC ++ 2017.
What is it and how to write them?
(Upd: the current version of Coroutines TS is n4775 from 2018-10-07)
Recently, a coroutine technical specification, Coroutines TS , has reached the "published" state. Coroutines are implemented in MS VC ++ 2017.
What is it and how to write them?
(Upd: the current version of Coroutines TS is n4775 from 2018-10-07)
In C ++, a coroutine is a function in which co_await , co_yield , co_return .
Future coroutine(X x) { Y y = co_await f(x); co_return y; } The compiler rewrites the coroutine's body, turning it into a state machine.
Under these coroutines, memory is allocated using the new operator.
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; } At the same time, although the coroutine’s body changes, it remains a function.
The compiler adds an implicit variable of type Future::promise_type .
This variable is used to create the result of the coroutine function ( p.get_return_object ), exception handling, co_return implementation. The compiler also adds suspension points at the beginning and end of the coroutine, wrapping the coroutine body into the following code:
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(); You cannot use return y; in a coroutine return y; , instead, co_return y; which is replaced by
p.return_value(y); goto final_suspend; If the coroutine does not co_return; to return the value on completion, then co_return; used co_return; (without expression) and the corresponding function p.return_void(); .
It is not necessary to write co_return; at the end of the coroutine.
Corouting is suspended in the co_await .
Code Y y = co_await f(x); replaced by
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(); The standard library provides the coroutine_handle class, which allows you to resume a suspended coroutine.
The function f takes it via e.await_suspend(h) . When the value of y is calculated, it should call h.resume() , and return the calculated value via e.await_resume() .
For stackless coroutines, the function f can be written as follows:
// Общие данные фонового потока и 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}; } For stackful coroutines (for example, Fibers on Windows), await_suspend should freeze the stream itself ( SwitchToFiber ). The resume point will be inside await_suspend , so it must return false .
The coroutine's minimum return type is:
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 must have get_return_object , initial_suspend , final_suspend and either return_void or return_value .
To implement initial_suspend and final_suspend you can use standard suspend_never and suspend_always , which return true and false values in await_ready respectively.
Such coroutine will always fall asleep at the end.
From the Future itself, it is only required that it delete the coroutine through h.destroy() .
Future can (but does not have to) repeat the Awaiter interface to be compatible with co_await .
For the expression a in co_await a , additional transformations can be applied:
p.await_transform(a) valid, then a is replaced by p.await_transform(a) ;a there is operator operator co_await , then a is replaced by operator co_await(a) ;Thus the variant is possible.
auto& e = operator co_await(p.await_transform(f(x))); if (!e.await_ready()) { ... } For example, you can define operator co_await(std::chrono::duration) and write co_await 10ms; .
A CoroutineState object is created with new . However, if there is a p.get_return_object_on_allocation_failure() function, the following code will be generated:
auto* s = new(std::nothrow) CoroutineState; if (!s) { return p.get_return_object_on_allocation_failure(); } auto result = s->p.get_return_object(); This allows you to handle memory allocation errors.
Also, coroutine arguments can participate in memory allocation.
For the Future coro(A1 a1, A2 a2) , if there is an operator new(std::size_t, A1, A2) function operator new(std::size_t, A1, A2) , then it will be called instead of the new operator by default.
The compiler uses the coroutine_traits class to get the promise_type .
For the Future coro(A1 a1, A2 a2) type std::experimental::coroutine_traits<Future, A1, A2>::promise_type will be used.
The default implementation is Future::promise_type , however this can be overridden by the user.
co_yield e; equivalent to co_await p.yield_value(e); and is used in generators - special coroutines that are designed to produce a sequence of values.
Source: https://ru.stackoverflow.com/questions/702454/
All Articles