C++ Coroutines Understanding the promise type

来源:https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type

Coroutine Concepts

The compiler applies some fairly mechanical transformations to the code that you write to turn it into a state-machine that allows it to suspend execution at particular points within the function and then later resume execution.

编译器会对您编写的代码进行一些相当机械的转换,以将其转换为状态机,使状态机可以在函数内的特定点挂起执行,然后再恢复执行。

Promise objects

The Promise object defines and controls the behaviour of the coroutine itself by implementing methods that are called at specific points during execution of the coroutine.

Before we go on, I want you to try and rid yourself of any preconceived notions of what a “promise” is. While, in some use-cases, the coroutine promise object does indeed act in a similar role to the std::promise part of a std::future pair, for other use-cases the analogy is somewhat stretched. It may be easier to think about the coroutine’s promise object as being a “coroutine state controller” object that controls the behaviour of the coroutine and can be used to track its state.

在继续之前,我希望您尝试摆脱对“promise”的任何先入为主的观念。虽然在某些用例中,协程的 promise 对象确实起到了与 std::futurestd::promise 部分相似的作用,但在其他用例中,类推却有些捉襟见肘。将协程的 promise 对象视为控制协程行为并可以用来跟踪其状态的“协程状态控制器”对象可能更容易些。

When you write a coroutine function that has a body, <body-statements>, which contains one of the coroutine keywords (co_return, co_await, co_yield) then the body of the coroutine is transformed to something (roughly) like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
co_await promise.initial_suspend();
try
{
<body-statements>
}
catch (...)
{
promise.unhandled_exception();
}
FinalSuspend:
co_await promise.final_suspend();
}

When a coroutine function is called there are a number of steps that are performed prior to executing the code in the source of the coroutine body that are a little different to regular functions.

Here is a summary of the steps (I’ll go into more detail on each of the steps below).

  1. Allocate a coroutine frame using operator new (optional).
  2. Copy any function parameters to the coroutine frame.
  3. Call the constructor for the promise object of type, P.
  4. Call the promise.get_return_object() method to obtain the result to return to the caller when the coroutine first suspends. Save the result as a local variable.
  5. Call the promise.initial_suspend() method and co_await the result.
  6. When the co_await promise.initial_suspend() expression resumes (either immediately or asynchronously), then the coroutine starts executing the coroutine body statements that you wrote.

Some additional steps are executed when execution reaches a co_return statement:

  1. Call promise.return_void() or promise.return_value(<expr>)
  2. Destroy all variables with automatic storage duration in reverse order they were created.
  3. Call promise.final_suspend() and co_await the result.

If instead, execution leaves <body-statements> due to an unhandled exception then:

  1. Catch the exception and call promise.unhandled_exception() from within the catch-block.
  2. Call promise.final_suspend() and co_await the result.

Once execution propagates outside of the coroutine body then the coroutine frame is destroyed. Destroying the coroutine frame involves a number of steps:

  1. Call the destructor of the promise object.
  2. Call the destructors of the function parameter copies.
  3. Call operator delete to free the memory used by the coroutine frame (optional)
  4. Transfer execution back to the caller/resumer.

When execution first reaches a <return-to-caller-or-resumer> point inside a co_await expression, or if the coroutine runs to completion without hitting a <return-to-caller-or-resumer> point, then the coroutine is either suspended or destroyed and the return-object previously returned from the call to promise.get_return_object() is then returned to the caller of the coroutine.

之后,作者就上述步骤进行了详细的介绍。

How the compiler chooses the promise type

The type of the promise object is determined from the signature of the coroutine by using the std::experimental::coroutine_traits class.

模板类的参数,就是协程函数签名中的返回类型+入参类型。

模板类 coroutine_traits 的默认实现,是使用返回类型的嵌套类型或类型别名:

using promise_type = typename RET::promise_type;

如果有权限,则在返回值类型中定义嵌套类即可;否则新增 coroutine_traits 特例化。

Identifying a specific coroutine activation frame

When you call a coroutine function, a coroutine frame is created. In order to resume the associated coroutine or destroy the coroutine frame you need some way to identify or refer to that particular coroutine frame.

The mechanism the Coroutines TS provides for this is the coroutine_handle type.

You can obtain a coroutine_handle for a coroutine in two ways:

  1. It is passed to the await_suspend() method during a co_await expression.
  2. If you have a reference to the coroutine’s promise object, you can reconstruct its coroutine_handle using coroutine_handle<Promise>::from_promise().

Note that the coroutine_handle is NOT and RAII object. You must manually call .destroy() to destroy the coroutine frame and free its resources.