问题
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 :(
回答1:
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:
- The coroutine Promise
- The coroutine Context
- The coroutine Future
- The coroutine Handle
- The coroutine itself
- 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_transformmethod in our
promise_type` which will transform
expr into an Awaiter.
Now that we have descibed 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.
来源:https://stackoverflow.com/questions/57621168/what-are-the-mechanics-of-coroutines-in-c20