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)

1 answer 1

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.

promise_type (part 1)

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(); 

co_return

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.

co_await

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 .

promise_type (part 2)

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 .

await_transform and operator co_await

For the expression a in co_await a , additional transformations can be applied:

  • if the expression p.await_transform(a) valid, then a is replaced by p.await_transform(a) ;
  • if for type a there is operator operator co_await , then a is replaced by operator co_await(a) ;
  • if the result is a prvalue , then it is copied to a temporary variable, otherwise it is used as is.

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; .

Memory allocation

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.

promise_type (part 3), coroutine_traits

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

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.

Examples

  • The question of necessity lags behind. Suppose I have 10 actions that I want to do in one thread. Some of them have some dependence on others, some don't, it doesn't matter. I can write to perform these 10 actions by the flow, and they will be executed. Or I can write a few ko-rutin to sub-execute (without breaking dependencies), add new abstractions to the code, complicate the code. As a result, the same 10 actions will be performed in one thread. The question is, why do I need any quails then? - Arkady
  • Korutiny needed for asynchronous action. If you can do everything synchronously, then you do not need them. - Abyx
  • so Korutin also do not give asynchrony. Actions will still be performed in the same thread. As a result, asynchronously, 10 actions did not distribute the execution time of the stream, it would be all the same 10 actions performed by the stream. the only possible application that I can see is, perhaps, interaction with external sources of signals, from the UI, which needs to be updated and listened to periodically, to some sockets, but in this case you will still have to synchronize access to some variables, and synchronization costs can not be avoided. So why not use streams? - Arkady
  • 3
    I therefore ask that I do not know, but I want to know why they were introduced) - Arkady
  • one
    Probably, for better understanding, you need some kind of "complete" and logically simple (but not too artificial, for example, something like a parser interacting with a lexer on one page) an example of 2 (or 3, including main? ) with coroutines (and explanations, we start here, we renew it in this cycle and use the next result like this, but we end it this way) Then maybe clarifying questions will appear at least. I, until I didn’t understand how to use all this. - avp