Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the mechanics of coroutines in C++20?

I was trying to read the documentation (cppreference and the standard documentation on the feature itself) on the sequence of operations that get called when a coroutine function is called, suspended, resumed and terminated. The documentation goes into depth outlining the various extension points that allow library developers to customize the behavior of their coroutine using library components. At a high-level, this language feature seems to be extremely well thought out.

Unfortunately, I'm having a really hard time following the mechanics of coroutine execution and how I, as a library developer, can use the various extension points to customize execution of said coroutine. Or even where to start.

The following functions are in the set of new customization-points that I do not fully understand:

  • initial_suspend()
  • return_void()
  • return_value()
  • await_ready()
  • await_suspend()
  • await_resume()
  • final_suspend()
  • unhandled_exception()

Can someone describe in high-level psuedocode, the code that the compiler generates when running a user coroutine? At a abstract level, I'm trying to figure out when functions like await_suspend, await_resume, await_ready, await_transform, return_value, etc are called, what purpose they serve and how I can use them to write coroutine libraries.


Not sure if this is off-topic, but some introductory resource here would be extremely helpful for the community in general. Googling around and diving into library implementations like in cppcoro is not helping me move past this initial barrier :(

like image 579
Curious Avatar asked Aug 23 '19 06:08

Curious


People also ask

What are coroutines in C?

Coroutines are general control structures where flow control is cooperatively passed between two different routines without returning.

What is the point of coroutines?

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

What do you mean by coroutines?

A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.

Are coroutines asynchronous C++?

C++ coroutines can also be used for asynchronous programming by having a coroutine represent an asynchronous computation or an asynchronous task.


1 Answers

N4775 outlines the proposal for coroutines for C++20. It introduces an number of different ideas. The following is from my blog at https://dwcomputersolutions.net . More info can be found in my other posts.

Before we examine our whole Hello World coroutine program, go through the various parts step-by-step. These include:

  1. The coroutine Promise
  2. The coroutine Context
  3. The coroutine Future
  4. The coroutine Handle
  5. The coroutine itself
  6. The subroutine that actually uses the coroutine

The entire file is included at the end of this post.

The Coroutine

Future f() {     co_return 42; } 

We instantiate our coroutine with

    Future myFuture = f(); 

This is a simple coroutine that just returns the value 42. It is a coroutine because it includes the keyword co_return. Any function that has the keywords co_await, co_return or co_yield is a coroutine.

The first thing you will notice is that although we are returning an integer, the coroutine return type is (a user defined) type Future. The reason is that when we call our coroutine, we don't run the function right now, rather we initialize an object which will eventually get us the value we are looking for AKA our Future.

Finding the Promised Type

When we instantiate our coroutine, the first thing the compiler does is find the promise type that represents this particular type of coroutine.

We tell the compiler what promise type belongs to what coroutine function signature by creating a template partial specialization for

template <typename R, typename P...> struct coroutine_trait {}; 

with a member called promise_type that defines our Promise Type

For our example we might want to use something like:

template<> struct std::experimental::coroutines_v1::coroutine_traits<Future> {     using promise_type = Promise; }; 

Here we create a specialization of coroutine_trait specifies no parameters and a return type Future, this exactly matches our coroutine function signature of Future f(void). promise_type is then the promise type which in our case is the struct Promise.

Now are a user, we normally will not create our own coroutine_trait specialization since the coroutine library provides a nice simple way to specify the promise_type in the Future class itself. More on that later.

The Coroutine Context

As mentioned in my previous post, because coroutines are suspend-able and resume-able, local variables cannot always be stored in the stack. To store non-stack-safe local variables, the compiler will allocate a Context object on the heap. An instance of our Promise will be stored as well.

The Promise, the Future and the Handle

Coroutines are mostly useless unless they are able to communicate with the outside world. Our promise tells us how the coroutine should behave while our future object allow other code to interact with the coroutine. The Promise and Future then communicate with each-other via our coroutine handle.

The Promise

A simple coroutine promise looks something like:

struct Promise  {     Promise() : val (-1), done (false) {}     std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }     std::experimental::coroutines_v1::suspend_always final_suspend() {         this->done = true;         return {};      }     Future get_return_object();     void unhandled_exception() { abort(); }     void return_value(int val) {         this->val = val;     }          int val;     bool done;     };  Future Promise::get_return_object() {     return Future { Handle::from_promise(*this) }; } 

As mentioned, the promise is allocate when the coroutine is instantiated and exits for the entire lifetime of the coroutine.

Once done, the compiler calls get_return_object This user defined function is then responsible for creating the Future object and returning it to the coroutine instatiator.

In our instance, we want our Future to be able to communicate with our coroutine so we create our Future with the handle for our coroutine. This will allow our Future to access our Promise.

Once our coroutine is created, we need to know whether we want to start running it immediately or whether we want it to remain suspended immediately. This is done by calling the Promise::initial_suspend() function. This function returns an Awaiter which we will look in another post.

In our case since we do want the function to start immediately, we call suspend_never. If we suspended the function, we would need to start the coroutine by calling the resume method on the handle.

We need to know what to do when the co_return operator is called in the coroutine. This is done via the return_value function. In this case we store the value in the Promise for later retrieval via the Future.

In the event of an exception we need to know what to do. This is done by the unhandled_exception function. Since in our example, exceptions should not occur, we just abort.

Finally, we need to know what to do before we destroy our coroutine. This is done via the final_suspend function In this case, since we want to retrieve the result so we return suspend_always. The coroutine must then be destroyed via the coroutine handle destroy method. Otherwise, if we return suspend_never the coroutine destroys itself as soon as it finishes running.

The Handle

The handle give access to the coroutine as well as its promise. There are two flavours, the void handle when we do not need to access the promise and the coroutine handle with the promise type for when we need to access the promise.

template <typename _Promise = void> class coroutine_handle;  template <> class coroutine_handle<void> { public:     void operator()() { resume(); }     //resumes a suspended coroutine     void resume();     //destroys a suspended coroutine     void destroy();     //determines whether the coroutine is finished     bool done() const; };  template <Promise> class coroutine_handle : public coroutine_handle<void> {     //gets the promise from the handle     Promise& promise() const;     //gets the handle from the promise     static coroutine_handle from_promise(Promise& promise) no_except; }; 

The Future

The future looks like this:

class [[nodiscard]] Future { public:     explicit Future(Handle handle)         : m_handle (handle)      {}     ~Future() {         if (m_handle) {             m_handle.destroy();         }     }     using promise_type = Promise;     int operator()(); private:     Handle m_handle;     };  int Future::operator()() {     if (m_handle && m_handle.promise().done) {         return m_handle.promise().val;     } else {         return -1;     } } 

The Future object is responsible for abstracting the coroutine to the outside world. We have a constructor that takes the handle from the promise as per the promise's get_return_object implementation.

The destructor destroys the coroutine since in our case it is the future that control's the promise's lifetime.

lastly we have the line:

using promise_type = Promise; 

The C++ library saves us from implementing our own coroutine_trait as we did above if we define our promise_type in the return class of the coroutine.

And there we have it. Our very first simple coroutine.

Full Source

  #include <experimental/coroutine> #include <iostream>  struct Promise; class Future;  using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;  struct Promise  {     Promise() : val (-1), done (false) {}     std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }     std::experimental::coroutines_v1::suspend_always final_suspend() {         this->done = true;         return {};      }     Future get_return_object();     void unhandled_exception() { abort(); }     void return_value(int val) {         this->val = val;     }          int val;     bool done;     };  class [[nodiscard]] Future { public:     explicit Future(Handle handle)         : m_handle (handle)      {}     ~Future() {         if (m_handle) {             m_handle.destroy();         }     }     using promise_type = Promise;     int operator()(); private:     Handle m_handle;     };  Future Promise::get_return_object() {     return Future { Handle::from_promise(*this) }; }   int Future::operator()() {     if (m_handle && m_handle.promise().done) {         return m_handle.promise().val;     } else {         return -1;     } }  //The Co-routine Future f() {     co_return 42; }  int main() {     Future myFuture = f();     std::cout << "The value of myFuture is " << myFuture() << std::endl;     return 0; } 

##Awaiters

The co_await operator allows us to suspend our coroutine and return control back to the coroutine caller. This allows us to do other work while waiting our operation completes. When they do complete, we can resume them from exactly where we left off.

There are several ways that the co_await operator will process the expression on its right. For now, we will consider the simplest case and that is where our co_await expression returns an Awaiter.

An Awaiter is a simple struct or class that implements the following methods: await_ready, await_suspend and await_resume.

bool await_ready() const {...} simply returns whether we are ready to resume our coroutine or whether we need to look at suspending our coroutine. Assuming await_ready returns false. We proceed to running await_suspend

Several signatures are available for the await_suspend method. The simplest is void await_suspend(coroutine_handle<> handle) {...}. This is the handle for the coroutine object that our co_await will suspend. Once this function completes, control is returned back to caller of the coroutine object. It is this function that is responsible for storing the coroutine handle for later so that our coroutine does not stay suspended forever.

Once handle.resume() is called; await_ready returns false; or some other mechanism resumes our coroutine, the method auto await_resume() is called. The return value from await_resume is the value that the co_await operator returns. Sometimes it is impractical for expr in co_await expr to return an awaiter as described above. If expr returns a class the class may provide its own instance of Awaiter operator co_await (...) which will return the Awaiter. Alternatively one can implement an await_transform method in our promise_type which will transform expr into an Awaiter.

Now that we have described Awaiter, I would like to point out that the initial_suspend and final_suspend methods in our promise_type both return Awaiters. The object suspend_always and suspend_never are trivial awaiters. suspend_always returns true to await_ready and suspend_never returns false. There is nothing stopping you from rolling out your own though.

If you are curious what a real life Awaiter looks like, take a look at my future object. It stores the coroutine handle in a lamda for later processing.

like image 155
doron Avatar answered Sep 20 '22 19:09

doron