Painless C++ Coroutines-Part 2

As promised, I continue the journey of demistifying coroutines from where we left off, i.e. Painless Coroutines-Part 1 where we covered sections 1 till 4. If you still haven’t looked at the first part, I strongly encourage you to do so before getting on with the second part. In this part, I resume the journey with the section that is somewhat a tautology, i.e., Resuming the coroutine ;) .

5. Resuming the coroutine

With most of the details about coroutine creation and suspension out of the way, we can finally look at more details of coroutine implementation that sheds a light on how a coroutine can be resumed after suspension.

5.1 The corutine handle

What I did not mention about co_await operator that apart from co awaiting the result of the awaiter object, it also saves the execution state of the coroutine on heap and creates a callable object also known as the coroutine handle which when invoked resumes the coroutine from its suspension state.

The standard provides the coroutine handle that we’re interested in and is a template object which can have two template parameters

  1. std::coroutine_handle<> : i.e., empty or void template parameter
  2. std::coroutine_handle<promise_type>

Another important aspect of these two types is that the first type can be implicitly converted from the second type. We will look into this in detail later but for now we are going to work mostly with the empty template version of the coroutine handle, except for briefly using the templated version once in the next subsection.

5.2 coroutine handle and the promise_type

The coroutine handle is associated with promise_type object for every coroutine execution and is of the type with template parameter mentioned in section 5.1, i.e., std::coroutine_handle<promise_type>.

Secondly the coroutine handle provides a static method from_promise to access the coroutine handle associated with the promise_type, i.e.

struct ReturnObject {        struct promise_type {
...
auto getHandle()
{
return std::coroutine_handle<promise_type>::from_promise(*this);
}
};
};

Unfortunately the static method, from_promise is only provided for the template version of coroutine handle and it makes total sense to do so since this method should be able to create a handle which is associated with the promise_type which is what is needed in our case.

Using the above fact and that template version of coroutine handle can be implicitly converted to non-template version, a coroutine handle object can be crated within the ReturnObject and eventually accessed outside the coroutine to resume it from its suspended state. Before moving forward, it may be useful to revisit section 4 .

5.3 Resuming a suspended coroutine

Lets look at another example of suspended coroutine state. Note the usage of std::suspend_never in the example below and recall the fact that this prevents the coroutine to be suspended when co_awaited (eiher implicitly or expicitly). The example below

struct promise_type{
...
std::suspend_never initial_suspend() {return {};}//coroutine not suspended initially
...
};
ReturnObject foo(){
std::cout << "1. hello from coroutine\n";
co_await std::suspend_never{}; //do not suspend the coroutine at this point
std::cout << "2. hello from coroutine\n";
co_await std::suspend_always{}; //suspend the coroutine at this point
std::cout << "3. hello from coroutine\n";
}
int main(){foo(); }

will not print the 3rd hello statement after it reaches the second co_await, unless the coroutine handle is accessed in main and called. More important to think here is how can the coroutine handle be accessed before the coroutine is suspended immediately upon its initial execution in main.

From section 4, it is clear that the coroutine may be suspended by promise_type::initial_suspend. It may not be, but if it is then it is best to access the coroutine before it gets suspended by implicit call to promise_type::initial_suspend. Looking back at the sequence of implict calls that happen under the hood during coroutine instantiation in section 4, the method promise_type::get_return_object could be utilized to create the access to coroutine handle outside of the coroutine before it is suspended. To do this,

A promise_type::get_return_object has to be modified to call the constructor of ReturnObject (see B.2 below) by using the static method from_promise which returns the underlying coroutine handle associated with the promise_type object.

B ReturnObject has to be modified in the following ways:

  1. ReturnObject has to have a member of type std::coroutine_handle<> h_; which is the handle to the coroutine in its suspended state.
  2. An explict constructor taking std::coroutine_handle<> as parameter and initializing h_.
  3. An implicit conversion operator that converts ReturnObject to std::coroutine_handle<promise_type> object and returns h_.

The modified part code for ReturnObject looks like below

struct ReturnObject {    struct promise_type {        //A
ReturnObject get_return_object()
{
//call ctor of ReturnObject with the coroutine handle
return {std::coroutine_handle<promise_type>::from_promise(*this)};
}
...
...
...
};
std::coroutine_handle<> h_; //B.1: member variable
ReturnObject(std::coroutine_handle<> h):h_{h}{ } //B.2, ctor //implicit conversion operator
operator std::coroutine_handle<promise_type>() const { return h_; } //B.3
};

With the above modifications the coroutine foo can be resumed after suspension by invoking the handle from main. For this first create the handle to coroutine by call to foo and making use of the implicit conversion operator marked B.3 like below:

int main
{
std::coroutine_handle<> h = foo();
//resume coroutine
}

Finally, to resume the coroutine to be able to print the remaining two hello statements simply call the coroutine handle like below:

    h(); //prints: 2. hello from coroutine
h.resume(); //same as h(), prints : 3. hello from coroutine

The entire code (but slightly more verbose) example can be accessed in this link.

6. Awaitables and Awaiter: a detour

Till now we’ve understood how to create, suspend and resume a coroutines, passing controls back and forth between the caller and the coroutine. They are not useful in the sense that the coroutine doesn’t return a value to the caller on suspension. To be able to return data with the help of co_await operator, we need to dive deeper into Awaiters and Awaitables, which was first introduced in section 3 and is the focus of this section.

To recapitulate at the end of section 2.2 we introduced a pseudo-code

ReturnObject bar(){co_await expr;}

and mentioned that the compiler processes it by series of calls to decide whether to suspend the coroutine or not. We take a deeper dive in this section how the compiler process the expression and what kind of calls are happening under the hood. Quoting again the important points from cpp reference page’s the co_await expr section on the evaluation of co_await expr

  • First, expr is converted to an awaitable ...
  • then, the awaiter object is obtained (from the awaitable) ...
  • then, awaiter.await_ready() is called ...
  • awaiter.await_suspend(handle) is called ...
  • finally, awaiter.await_resume() is called ...

For the most simple cases that we considered till now, awaitable and expr are the same, for e.g. the trivial awaitables in the standard: std::suspend_never and std::suspend_always. Also for simple cases an awaitable and awaiter are also the same, lets see how.

6.1 awaiter can be same as awaitable

Recalling form the lines quoted above from cpp reference on evaluation of co_await expr, one can see the methods that are called on awaiter after it is obtained from an awaitable object, viz:

  1. bool awaiter::await_ready()
  2. void/bool awaiter::await_suspend(std::coroutine_handle<> h)
  3. void awaiter::await_resume()

i.e. an awaiter object has the above three methods as a part of their interface. On checking the cpp reference page for the trivial awaitables std::suspend_never and std::suspend_always it is clear that they also happen to have the same interface as that an awaiter.

Thus, again for the simple cases, awaiter is same as an awaitable.

Before the conclusion of the subsection, it must be noted here that the result of the expression co_await expr is the same as that of awaiter::await_resume. In all the examples domonstrated so far, this has been the same as std::suspend_always::await_resume, i.e., void

6.2 Dissceting awaiter- Part 1

Building upon coroutine example from section 5, lets create our own simple awaiter/awaitable and name it suspend_always similar to the one in standard, but again, with print statements to be able to track the implicit compiler calls to the interface, and co_await it in our foo coroutine

First our custom suspend_always object:

int ctr{0}; //global counterstruct suspend_always{
bool await_ready() const noexcept {
std::cout << ++ctr << ". suspend_always::await_ready\n";
return false;
}
void await_suspend(std::coroutine_handle<> h)const noexcept {
std::cout << ++ctr << ". suspend_always::await_suspend\n";
}
void await_resume() const noexcept {
std::cout << ++ctr << ". suspend_always::await_resume\n";
}
};

ReturnObject: remains unchanged as in section 5

Coroutine and main defined as:

ReturnObject foo(){
std::cout << ++ctr << ". hello from coroutine\n";
co_await suspend_always{}; //evaluate the custom awaiter
std::cout << ++ctr << ". hello from coroutine\n";
}
int main()
{
std::coroutine_handle<> h = foo();
}

The result of the call to fooin main prints the follwoing :

1. hello from coroutine
2. suspend_always::await_ready
3. suspend_always::await_suspend

What happened when our suspend_always object was co-awaited is that the compiler checked if the coroutine should be suspended or not by executing the suspend_always::await_ready method. In the above code, await_ready returned false, indicating that the coroutine should indeed be suspended. await_ready.

Next the suspend_always::await_suspend method is called by compiler with the coroutine handle. It is in this method one can execute asynchronous/asynchronous code while the coroutine is suspended or unsuspended.

Calling the coroutine handle h in main prints the two more statements

h(); //resume coroutine in mainstdout: 
1. first hello from coroutine
2. suspend_always::await_ready
3. suspend_always::await_suspend
4. suspend_always::await_resume
5. second hello from coroutine

The code can be found in this link.

If instead, suspend_always::await_ready returned true (try it!), then suspend_always::await_suspend is NOT executed and the coroutine is not suspended (causing the coroutine to behave like a void returning function, and the first call to foo in the main results in the print statement with both hello statements from within the coroutine.

await_ready is an optimization. Why? If await_ready returns false the method await_suspend is called with the coroutine handle to it and the coroutine state is saved by copying the values from register to coroutine farme, possibly on the heap. The reason this method is provided is incase await_ready returns true and coroutine is not suspended, the cost copying the local variables to the coroutine frame can be avoided and the coroutine state will not be saved.

6.3 Dissceting awaiter- Part 2

Here I want to talk about the two return types for await_suspend which are void and bool returning versions. The major difference arising out of this is the decision whether or not the coroutine execution is suspended:

(i) void await_suspend(std::coroutine_handle<> h) This type suspends the coroutine. This was already observed in the code example in section 6.1

(ii)bool await_suspend(std::coroutine_handle<> h) This type allows to conditionally suspend/resume the coroutine, i.e. when the statement

ReturnObject foo(){
...
co_await suspend_always{}; //evaluate the custom awaiter
}

is encountered and await_suspend returns bool , two scenarios can happen:

  • returning true causes suspends the coroutine to be suspended.
  • returning false causes the coroutine to NOT be suspended.

This can be exemplified by yet another simple example by modifying the await_suspend from example section 6.2

struct suspend_always{
...
bool await_suspend(std::coroutine_handle<> h)const noexcept {
std::cout << ++ctr << ". suspend_always::await_suspend\n";
return true;
}
...
};

Above modification with await_suspend returning true is the same as the void version. The functional code can be found here. If instead await_suspend returned false, it can be observed( here), that the coroutine continued without the need to be resumed after executing await_suspend method and the semantics of the custom suspend_always is similar to that of std::suspend_never. One can think of this as hijacking the suspension step of coroutine too.

The bool returning version of coroutine is useful when a developer wants to execute code synchronously after co_awating the awaiter and resume the coroutine after finishing the synchronous code. For e.g., one can call other methods within the await_suspend, launch another thread in it etc. For more details this article, that goes in detail of launching asynchronous as well as synchronous operations along with coroutines, can be referred. A practical example is presented in section 8 but I ask the readers to not jump to it for the time being.

The major take away of this subsection is that the standard has allowed room for library writers to customize the semantics of awaiters by providing two versions of await_suspend.

6.4 overloads of await_suspend

In the last subsection we saw two versions of await_suspend, i.e. a bool returning version and void returning version. In this section we look at two overloads of await_suspend in terms of the parameter it takes. The one that has already been covered was the overload taking std::coroutine_handle<>, or to be more pedantic, lets call it std::coroutine_handle<void>. Other one is also taking the coroutine handle as parameter but the one with promise_type template parameter which was briefly covered in section 5.1, i.e, std::coroutine_handle<promise_type >. Hence the awit_suspend method can be :

struct suspend_always{
...
bool/void
await_suspend(std::coroutine_handle<promise_type> h) {
return true;
}
...
};

But in reality this is not possible as one can see here, since promise_type is nested within ReturnObject and hence cannot be accessed directly. So the next logical approach is to change the template type to ReturnObject::promise_type , but this creates a bigger mess than what we just saw. The reason for this is straight forward, i.e., our custom suspend_always is using ReturnObject::promise_type before it was declared. So we happily exchange the definitions as so:

struct ReturnObject {    struct promise_type {
...
};
...
};
struct suspend_always{
...
};

This makes the code compile as can be seen here. This is okay but not flexible since in some cases using the await_suspend overload taking std::coroutine_handle<void> is sufficient and it requires writing duplicate code when the await_suspend overload taking std::coroutine_handle<promise_type> is also needed. Hence I would like to demonstrate another method that is more often used and is the standard among developers. It involves templates and is explained in the next subsection. Note: I will continue to use this method in the upcoming issues where ever required.

6.5 Templatized awaiter object

It’s time to turn the heat up and templatize the structure of suspend_always so that it can be parameterized with ReturnObject::promise_type . The reason for this is two folds: (1) to be able to use the overloads of await_suspend (as described in the last section) more flexibly. (2) to be able to somehow inject the data from promise_type within the awaiter and catch it outside the coroutine with the help of the coroutine handle which will be explored in detail in section 7.

Lets take a look at how this can be done.

  1. First modify the definition of suspend_always to make it a template class/struct like below:
template<typename PromiseType=void>
struct suspend_always{
...
...
void await_suspend(std::coroutine_handle<PromiseType> h);
};

In the snippet above, I added a default template parameter, void, to the awiater struct. ReturnObject remains unchanged as in section 5/6.2. Coroutine foo has now a minor change in the way suspend_always is used . For demonstration, I use it with both the overloads of template parameters and consequently suspend_always::await_suspend which was also the goal that we strived for at the end of section 6.4.

ReturnObject foo(){
...
co_await suspend_always<>{}; //note this
co_await suspend_always<ReturnObject::promise_type>{};//note this
...
}

The functional code can be found here.

Till this point, the exact same example in section 5.3 has been replicated with additional burden of templated awaiter. But this will be useful and necessary to be able to (like I mentioned before) inject data to awaiter from the promise_type and pass it outside the coroutine. Lets take a look at this in the next subsection.

Conclusion

In this issue of the Painless C++ coroutines, I gradually increased the complexity of tutorial with as many examples as I could come up with and reached a point where the end of the long journey to uncover the mysteries of corutine with co_await seems nearer.

In the next issue, Painless C++ Coroutine-Part 3, I’ll continue where I left off and explain how to make a data available to the caller (for e.g. main)of the cououtine by inject the promise_typedata into the coroutine foo , modify it within foo and make it available outside the coroutine. Till then I encourage the readers to play with the examples or be creative and come up with their own examples.

Support me by becoming a member

If you like my tutorials and articles, please consider supporting me by becoming a member through my medium referral link and get unlimited access to all articles on medium.com for just $5 a month.

--

--

--

I'm a backend software developer and from time to time I also like to explore web development

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Active Record Callbacks

GitOps in Kubernetes, the Easy Way

Django & Schedule Tasks

READ/DOWNLOAD%& Finite Element Analysis of Composi

What We Learnt At The ScaleConf 2017 Scalability Summit

6 Simple And Effective Tips For Fintech Mobile App Developers

Can We Still Invent Sort Algorithms?

Easy deployment of AI microservices

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gajendra Gulgulia

Gajendra Gulgulia

I'm a backend software developer and from time to time I also like to explore web development

More from Medium

C++20 Concurrency: part-3 request_stop and stop_token for std::jthread

C++20 — Practical Coroutines

C++ Pointers: How to write clean code with a Pointer to Pointer!

ListNode definition and it’s mental model

Introduction to C Programming