Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
josuttis_nm_c20_the_complete_guide.pdf
Скачиваний:
53
Добавлен:
27.03.2023
Размер:
5.85 Mб
Скачать

536

Chapter 15: Coroutines in Detail

Initially, the three coroutines started have the same priority. Because the priority of a coroutine is reduced each time it is suspended, the other coroutines then run in succession until the first coroutine has the highest priority again.

15.7.3 Symmetric Transfer with Awaiters for Continuation

The return type of await_suspend() can be a coroutine handle. In that case, a coroutine is suspended by immediately resuming the returned coroutine. This technique is called symmetric transfer.

Symmetric transfer was introduced to improve performance and avoid stack overflows when coroutines call other coroutines. In general, when resuming a coroutine with resume(), the program needs a new stack frame for the new coroutine. If we were to resume a coroutine inside await_suspend() or after it was called, we would always pay that price. By returning the coroutine to be called, the stack frame for the current coroutine just replaces its coroutine so that no new stack frame is required.

Implementing Symmetric Transfer with Continuations

The typical application of this technique is implemented by using final awaiters that deal with continuations.8

Assuming that you have a coroutine that ends and should then continue with another subsequent coroutine without giving control back to the caller, you run into the following problem: inside final_suspend(), your coroutine is not in a suspended state yet. You have to wait until await_suspend() is called for the returned awaitable to deal with any resumption. That might look as follows:

class CoroTask

{

public:

struct promise_type;

using CoroHdl = std::coroutine_handle<promise_type>;

private:

 

CoroHdl hdl;

// native coroutine handle

public:

 

struct promise_type {

std::coroutine_handle<> contHdl = nullptr; // continuation (if there is one)

...

auto final_suspend() noexcept {

// the coroutine is not suspended yet, use awaiter for continuation return FinalAwaiter{};

}

};

...

};

Thus, we return an awaiter that can be used to deal with the coroutine after it has been suspended. Here, the returned awaiter has the type FinalAwaiter, which might look as follows:

8This technique is documented with tremendous help from articles and emails by Lewis Baker and Michael Eiler. http://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer in particular provides a detailed motivation and explanation.

15.7 co_await and Awaiters in Detail

537

struct FinalAwaiter {

bool await_ready() noexcept { return false;

}

std::coroutine_handle<> await_suspend(CoroTask::CoroHdl h) noexcept {

//the coroutine is now suspended at the final suspend point

//- resume its continuation if there is one

if (h.promise().contHdl) {

 

return h.promise().contHdl;

// return the next coro to resume

}

 

else {

 

return std::noop_coroutine();

// no next coro => return to caller

}

 

}

 

void await_resume() noexcept {

 

}

 

};

Because await_suspend() returns a coroutine handle, the coroutine returned is automatically resumed on suspension. In that case, the utility function std::noop_coroutine() signals not to resume any other coroutine, meaning that the suspended coroutine returns to the caller.

std::noop_coroutine() returns a std::noop_coroutine_handle, which is an alias type of std::coroutine_handle<std::noop_coroutine_promise>. Coroutines of that type have no effect when calling resume() or destroy(), return nullptr when calling address(), and always return false when calling done().

std::noop_coroutine() and its return type are provided for situations in which await_suspend() may optionally return a coroutine to continue with. Having return type std::coroutine_handle<>, await_suspend() can return a noop coroutine to signal not to automatically resume another coroutine.

Note that two different return values of std::noop_coroutine() do not necessarily compare as equal. Therefore, the following code does not work portably:

std::coroutine_handle<> coro = std::noop_coroutine();

...

if (coro == std::noop_coroutine()) { // OOPS: does not check whether coro has initial value

...

return coro;

}

You should use nullptr instead:

std::coroutine_handle<> coro = nullptr;

...

if (coro) { // OK (checks whether coro has initial value)

...

return std::noop_coroutine();

}

Dealing with coroutines in a thread pool demonstrates an application of this technique.

538

Chapter 15: Coroutines in Detail

15.8Other Ways of Dealing with co_await

So far, we have passed only awaiters to co_await. For example, by using a standard awaiter:

co_await std::suspend_always{};

Or, as another example, co_await took a user-defined awaiter:

co_await CoroPrio{CoroPrioRequest::less}; // SUSPEND with lower prio

However, co_await expr is an operator that can be part of a bigger expression and take values that do not have an awaiter type. For example:

co_await 42; co_await (x + y);

The co_await operator has the same priority as sizeof or new. For this reason, you cannot skip the parentheses in the example above without changing the meaning. The statement

co_await x + y; would be evaluated as follows:

(co_await x) + y;

Note that x + y or just x does not have to be an awaiter here. co_await needs an awaitable and awaiters are only one (typical) implementation of them. In fact, co_await accepts any value of any type provided there is a mapping to the API of an awaiter. For the mapping, the C++ standard provides two approaches:

The promise member function await_transform()

operator co_await()

Both approaches allow coroutines to pass any value of any type to co_await specifying an awaiter implicitly or indirectly.

15.8.1 await_transform()

If a co_await expression occurs in a coroutine, the compiler first looks up whether there is a member function await_transform() that is provided by the promise of the coroutine. If that is the case, await_transform() is called and has to yield an awaiter, which is then used to suspend the coroutine.

For example, this means that: class CoroTask {

struct promise_type {

...

auto await_transform(int val) { return MyAwaiter{val};

}

};

...

};

15.8 Other Ways of Dealing with co_await

539

CoroTask coro()

{

co_await 42;

}

has the same effect as:

class CoroTask {

...

};

CoroTask coro()

{

co_await MyAwaiter{42};

}

You can also use it to enable a coroutine to pass a value to the promise before using a (standard) awaiter:

class CoroTask { struct promise_type {

...

auto await_transform(int val) {

... // process val

return std::suspend_always{};

}

};

...

};

CoroTask coro()

{

co_await 42; // let 42 be processed by the promise and suspend

}

Using Values to Let co_await Update Running Coroutines

Remember the example in which an awaiter was used to change the priority of the coroutine. There, we used an awaiter of the type CoroPrio to enable the coroutine to request the new priority as follows:

co_await CoroPrio{CoroPrioRequest::less}; // SUSPEND with lower prio

We could also allow only the new priority to be passed:

co_await CoroPrioRequest::less;

// SUSPEND with lower prio

All we would need is for the coroutine interface promise to provide a member function await_transform() for values of this type:

class CoroPrioTask { public:

struct promise_type;

using CoroHdl = std::coroutine_handle<promise_type>;

540

Chapter 15: Coroutines in Detail

private:

 

CoroHdl hdl;

// native coroutine handle

friend class CoroPrioScheduler;

// give access to the handle

public:

 

struct promise_type {

 

CoroPrioScheduler* schedPtr = nullptr;

// each task knows its scheduler:

...

 

auto await_transform(CoroPrioRequest);

// deal with co_await CoroPrioRequest

};

 

...

 

};

 

The implementation of await_transform() may look as follows:

inline auto CoroPrioTask::promise_type::await_transform(CoroPrioRequest pr) { auto hdl = CoroPrioTask::CoroHdl::from_promise(*this); schedPtr->changePrio(hdl, pr);

return std::suspend_always{};

}

Here, we again use the static member function from_promise() for coroutine handles to get the handle, because changePrio() needs the handle as its first argument.

That way, we can skip the awaiter CoroPrio as a whole. See coro/coropriosched2.hpp for the whole code (and coro/coroprio2.cpp and coro/coroprio2.hpp for its use).

Letting co_await Act Like co_yield

Remember the example with co_yield. To handle the value to be yielded we do the following:

struct promise_type {

 

int coroValue = 0;

// last value from co_yield

auto yield_value(int val) {

// reaction to co_yield

coroValue = val;

// - store value locally

return std::suspend_always{};

// - suspend coroutine

}

 

...

 

};

 

co_yield val; // calls yield_value(val) on promise

We can get the same effect with the following:

 

struct promise_type {

 

int coroValue = 0;

// last value from co_yield

auto await_transform(int val) {

 

coroValue = val;

// - store value locally

return std::suspend_always{};