本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第三篇,原文内容在https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type
This post is the third in the series on the C++ Coroutines TS (N4736).
这是C++协程文章系列的第三篇。
The previous articles in this series cover:
前面的两篇在:
In this post I look at the mechanics of how the compiler translates coroutine code that you write into compiled code and how you can customise the behaviour of a coroutine by defining your own Promise type.
在这篇文章中我将聚焦于编译器如何将你写的协程代码编译,以及你如何可以通过定义你的Promise类型来定制协程的行为。
Coroutine Concepts
The Coroutines TS adds three new keywords: co_await
, co_yield
and co_return
. Whenever you use one of these coroutine keywords in the body of a function this triggers the compiler to compile this function as a coroutine rather than as a normal function.
Coroutines TS增加了三个新关键字:co_await、co_yield和co_return。无论何时在函数体中使用这些协程关键字之一,都会触发编译器将此函数编译为协程而不是普通函数。
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.
编译器对你编写的代码应用一些相当机械的转换,将其转换为一个状态机,允许它在函数中的特定点暂停执行,然后再继续执行。
In the previous post I described the first of two new interfaces that the Coroutines TS introduces: The Awaitable interface. The second interface that the TS introduces that is important to this code transformation is the Promise interface.
在上一篇文章中,我描述了Coroutines TS引入的两个新接口中的第一个:Awaitable接口。TS引入的第二个接口是Promise接口,它对代码转换非常重要。
The Promise interface specifies methods for customising the behaviour of the coroutine itself. The library-writer is able to customise what happens when the coroutine is called, what happens when the coroutine returns (either by normal means or via an unhandled exception) and customise the behaviour of any co_await
or co_yield
expression within the coroutine.
Promise接口指定了定制协程本身行为的方法。库编写者能够定制调用协程时发生的行为,以及协程返回时发生的行为(通过正常方式或通过未处理的异常),并定制协程中任何co_await或co_yield表达式的行为。
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.
Promise对象通过实现(协程执行到特定位置调用的)方法来定义和控制协程本身的行为。
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”的任何先入为主的观念。虽然在某些用例中,coroutine promise对象的作用确实与std::future对中的std::promise部分类似,但对于其他用例,这种类比有些不符。把协程的promise对象看作是一个“协程状态控制器”对象,它控制着协程的行为,可以用来跟踪它的状态,这可能更容易。
An instance of the promise object is constructed within the coroutine frame for each invocation of a coroutine function.
promise对象的实例在每次调用协程函数时构造在协程帧内。
The compiler generates calls to certain methods on the promise object at key points during execution of the coroutine.
编译器在执行协程时,在关键位置对promise对象上的某些方法进行调用。
In the following examples, assume that the promise object created in the coroutine frame for a particular invocation of the coroutine is promise
.
在下面的示例中,假设在协程帧中为特定的协程调用创建的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:
当你编写具有函数体的协程函数——<body-statements>
时,其中包含一个协程关键字(co_return、co_await、co_yield),那么协程的函数体将转换为(大致)如下:
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.
1. 使用运算符new(可选)分配协程帧。
2. 将所有函数参数复制到协程帧。
3. 调用类型为P的promise对象的构造函数。
4. 调用promise.get_return_object()方法在首次挂起协程时获取返回主调函数的结果,将结果保存为本地变量。
5. 调用promise.initial_suspend()方法并co_await结果。
6. 当co_await promise.initial_suspend()表达式恢复执行(立即或异步地)时,然后协程开始执行你编写的coroutine body语句。
Some additional steps are executed when execution reaches a co_return
statement:
当执行到达co_return语句时,将执行一些附加步骤:
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.
1.调用promise.return_void()或promise.return_value(<expr>)
2.以与创建时相反的顺序销毁所有具有auto生命周期的变量
3.调用promise.final_suspend()和co_await结果
If instead, execution leaves <body-statements>
due to an unhandled exception then:
相反,如果执行由于未处理的异常而离开<body statements>
,则:
1. Catch the exception and call promise.unhandled_exception()
from within the catch-block.
2. Call promise.final_suspend()
and co_await
the result.
1.捕获异常并在异常块调用promise.unhandled_exception()
2.调用promise.final_suspend()并co_await结果
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.
1.调用promise对象的析构函数
2.调用(复制后)函数参数的析构函数
3.调用operator delete释放协程帧使用的内存(可选)
4.将执行权转移回主调函数/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.
当执行到达co_await表达式中的<return to caller or resumer>点时,或者如果协程运行到完成时没有触发<return to caller or resumer>点,则协程被挂起或销毁,并且先前从promise.get_return_object()的调用返回的return-object将返回给协程的主调函数。
Allocating a coroutine frame
First, the compiler generates a call to operator new
to allocate memory for the coroutine frame.
首先,编译器生成一个对operator new的调用,为协程帧分配内存。
If the promise type, P
, defines a custom operator new
method then that is called, otherwise the global operator new
is called.
如果promise类型P定义了一个自定义operator new方法,则调用该方法,否则调用全局operator new。
There are a few important things to note here:
这里有几点需要注意:
The size passed to operator new
is not sizeof(P)
but is rather the size of the entire coroutine frame and is determined automatically by the compiler based on the number and sizes of parameters, size of the promise object, number and sizes of local variables and other compiler-specific storage needed for management of coroutine state.
传递给operator new的大小不是sizeof§,而是整个协程帧的大小,由编译器根据参数的数量和大小、promise对象的大小、局部变量的数量和大小以及管理协程状态所需的其他编译器所需变量自动确定。
The compiler is free to elide the call to operator new
as an optimisation if:
如果出现以下情况,编译器可以省略对operator new的调用以作优化:
-
it is able to determine that the lifetime of the coroutine frame is strictly nested within the lifetime of the caller; and
-
the compiler can see the size of coroutine frame required at the call-site.
-
能够确定协程帧的生存周期严格嵌套在主调函数的生存周期内;和
-
编译器可以知道调用所需的协程帧的大小。
In these cases, the compiler can allocate storage for the coroutine frame in the caller’s activation frame (either in the stack-frame or coroutine-frame part).
在这些情况下,编译器可以在主调函数的活跃帧中为调用者中的协程帧(在堆栈帧或协程帧部分中)分配内存。
The Coroutines TS does not yet specify any situations in which the allocation elision is guaranteed, so you still need to write code as if the allocation of the coroutine frame may fail with std::bad_alloc
. This also means that you usually shouldn’t declare a coroutine function as noexcept
unless you are ok with std::terminate()
being called if the coroutine fails to allocate memory for the coroutine frame.
Coroutines TS 还没有指定任何保证分配可省略的情况,因此你仍然需要在编码时考虑协程帧的分配可能会出现像std::bad_alloc失败的情况。这也意味着你通常不应该将协程函数声明为noexcept,除非协程帧分配内存失败时调用std::terminate()对你来说可以接受。
There is a fallback, however, that can be used in lieu of exceptions for handling failure to allocate the coroutine frame. This can be necessary when operating in environments where exceptions are not allowed, such as embedded environments or high-performance environments where the overhead of exceptions is not tolerated.
但是,可以用回退代替异常来处理分配协程帧的失败。在不允许异常的环境中操作时,这是必要的,例如不允许异常开销的嵌入式环境或高性能环境。
If the promise type provides a static P::get_return_object_on_allocation_failure()
member function then the compiler will generate a call to the operator new(size_t, nothrow_t)
overload instead. If that call returns nullptr
then the coroutine will immediately call P::get_return_object_on_allocation_failure()
and return the result to the caller of the coroutine instead of throwing an exception.
如果promise类型提供静态P::get_return_object_on_allocation_failure()成员函数,则编译器将生成对运算符new(size_t, nothrow_t)重载的调用。如果该调用返回nullptr,那么协程将立即调用P::get_return_object_on_allocation_failure(),并将结果返回给协程的主调函数,而不是抛出异常。
Customising coroutine frame memory allocation
Your promise type can define an overload of operator new()
that will be called instead of global-scope operator new
if the compiler needs to allocate memory for a coroutine frame that uses your promise type.
如果编译器需要为使用promise类型的协程帧分配内存,那么promise类型可以定义操作符new()的重载,该重载将被调用,而不是全局作用域操作符new。
For example:
“But what about custom allocators?”, I hear you asking.
“但是定制内存分配器呢?”,我听到你在问。
You can also provide an overload of P::operator new()
that takes additional arguments which will be called with lvalue references to the coroutine function parameters if a suitable overload can be found. This can be used to hook up operator new
to call an allocate()
method on an allocator that was passed as an argument to the coroutine function.
你也可以提供一个接受额外参数的P::operator new()的重载,如果重载合适,它可以接受协程函数参数的左值引用。此时你可以将allocator作为协程函数的参数(传给operator new从而)在operator new中调用allocate方法。
You will need to do some extra work to make a copy of the allocator inside the allocated memory so you can reference it in the corresponding call to operator delete
since the parameters are not passed to the corresponding operator delete
call. This is because the parameters are stored in the coroutine-frame and so they will have already been destructed by the time that operator delete
is called.
你需要做一些额外的工作,在分配的内存中复制分配器,以便在对应的operator delete调用中引用它,这是因为参数没有传递给相应的operator delete调用(译注:new和delete要对称,不能只传给new allocator而不给delete释放方法)。这是因为参数存储在协程帧中,因此在调用操作符delete时,这些参数(指allocator)可能已经被破坏。
For example, you can implement operator new
so that it allocates extra space after the coroutine frame and use that space to stash a copy of the allocator that can be used to free the coroutine frame memory.
例如,您可以实现操作符new,这样它在协程帧之后分配额外空间,并使用该空间来存储分配器的副本,该(分配器)副本可用于释放协程帧内存。
For example:
To hook up the custom my_promise_type
to be used for coroutines that pass std::allocator_arg
as the first parameter, you need to specialise the coroutine_traits
class (see section on coroutine_traits
below for more details).
为将分配器std::allocator_arg作为promise的第一个参数传给promise类型来定制my_promise_type,你需要指定coroutine_traits类(下面的coroutine_traits一节将介绍更多细节)。
For example:
Note that even if you customise the memory allocation strategy for a coroutine, the compiler is still allowed to elide the call to your memory allocator.
注意,即使你为协程定制了内存分配策略,编译器仍然可以省略对内存分配器的调用。
Copying parameters to the coroutine frame
The coroutine needs to copy any parameters passed to the coroutine function by the original caller into the coroutine frame so that they remain valid after the coroutine is suspended.
协程需要将原始主调函数传递给协程函数的所有参数复制到协程帧中,以便在协程挂起后它们仍然有效。
If parameters are passed to the coroutine by value, then those parameters are copied to the coroutine frame by calling the type’s move-constructor.
如果参数是按值传递给协程的,那么这些参数将通过调用类型的move构造函数复制到协程帧中。
If parameters are passed to the coroutine by reference (either lvalue or rvalue), then only the references are copied into the coroutine frame, not the values they point to.
如果参数通过引用(左值引用或右值引用)传递给协程,那么只有引用被复制到协程帧中,它们所指向的值则不会被复制。
Note that for types with trivial destructors, the compiler is free to elide the copy of the parameter if the parameter is never referenced after a reachable <return-to-caller-or-resumer>
point in the coroutine.
注意,对于具有普通析构函数的类型,如果参数在协程中的可到达<return to caller or resumer>
点之后从未被引用,则编译器可以*地省略复制参数的过程。
There are many gotchas involved when passing parameters by reference into coroutines as you cannot necessarily rely on the reference remaining valid for the lifetime of the coroutine. Many common techniques used with normal functions, such as perfect-forwarding and universal-references, can result in code that has undefined behaviour if used with coroutines. Toby Allsopp has written a great article on this topic if you want more details.
当通过引用将参数传递到协程中时,会涉及到许多问题,因为在协程的生存期内引用不一定一直有效。与正常函数一起使用的许多常见技术,如完美转发和通用引用,如果与协程一起使用,可能会导致代码具有未定义的行为。如果你想了解更多细节,Toby Allsopp已经写了一篇关于这个话题的好文章。
If any of the parameter copy/move constructors throws an exception then any parameters already constructed are destructed, the coroutine frame is freed and the exception propagates back out to the caller.
如果任何参数copy/move构造器抛出异常,那么任何已经构造的参数都将被破坏,协程帧将被释放,异常将传播回主调函数。
Constructing the promise object
Once all of the parameters have been copied into the coroutine frame, the coroutine then constructs the promise object.
一旦所有的参数都被复制到协程帧中,协程就会构造promise对象。
The reason the parameters are copied prior to the promise object being constructed is to allow the promise object to be given access to the post-copied parameters in its constructor.
在构造promise对象之前复制参数的原因是允许promise对象访问其构造函数中复制后的参数。
First, the compiler checks to see if there is an overload of the promise constructor that can accept lvalue references to each of the copied parameters. If the compiler finds such an overload then the compiler generates a call to that constructor overload. If it does not find such an overload then the compiler falls back to generating a call to the promise type’s default constructor.
首先,编译器检查promise构造函数是否有可以接受对每个复制参数的左值引用的重载。如果编译器发现这样的重载,那么编译器将生成对该重载构造函数的调用。如果没有找到这样的重载,那么编译器将返回到生成对promise类型的默认构造函数的调用。
Note that the ability for the promise constructor to “peek” at the parameters was a relatively recent change to the Coroutines TS, being adopted in N4723 at the Jacksonville 2018 meeting. See P0914R1 for the proposal. Thus it may not be supported by some older versions of Clang or MSVC.
注意promise构造函数“窥视”参数的能力是对Coroutines TS的一个相对较新的更改,在Jacksonville 2018年会议的N4723中被采用。方案见P0914R1。因此,一些旧版本的Clang或MSVC可能不支持它。
If the promise constructor throws an exception then the parameter copies are destructed and the coroutine frame freed during stack unwinding before the exception propagates out to the caller.
如果promise构造函数抛出一个异常,那么在异常传播到主调函数之前,在栈展开期间释放协程帧,销毁参数副本。
Obtaining the return object
The first thing a coroutine does with the promise object is obtain the return-object
by calling promise.get_return_object()
.
协程对promise对象所做的第一件事是通过调用promise.get_return_object()获取return-object。
The return-object
is the value that is returned to the caller of the coroutine function when the coroutine first suspends or after it runs to completion and execution returns to the caller.
return-object是当协程第一次挂起或在运行到完成并且执行流返回给主调函数之后,返回给协程函数主调函数的值。
You can think of the control flow going something (very roughly) like this:
你可以将控制流想象成这样(非常粗略):
Note that we need to obtain the return-object before starting the coroutine body since the coroutine frame (and thus the promise object) may be destroyed prior to the call to coroutine_handle::resume()
returning, either on this thread or possibly on another thread, and so it would be unsafe to call get_return_object()
after starting execution of the coroutine body.
注意,我们需要在启动协程函数体之前获取return-object,因为在调用coroutine_handle::resume()返回之前,协程帧(以及promise对象)可能在这个线程上或另一个线程上被破坏,因此,在开始执行协同程序主体之后调用get_return_object()是不安全的。
The initial-suspend point
The next thing the coroutine executes once the coroutine frame has been initialised and the return object has been obtained is execute the statement co_await promise.initial_suspend();
.
一旦协程帧被初始化并获得return-object,协程执行的下一件事就是执行语句co_await promise.initial_suspend();。
This allows the author of the promise_type
to control whether the coroutine should suspend before executing the coroutine body that appears in the source code or start executing the coroutine body immediately.
这允许promise_type的作者在协程体源代码之前决定是应该挂起协程体,还是立即开始执行协程体。
If the coroutine suspends at the initial suspend point then it can be later resumed or destroyed at a time of your choosing by calling resume()
or destroy()
on the coroutine’s coroutine_handle
.
如果协程在初始挂起点挂起,那么你稍后可以通过调用协程的coroutine_handle的resume()或destroy()在选定的时间恢复或销毁它。
The result of the co_await promise.initial_suspend()
expression is discarded so implementations should generally return void
from the await_resume()
method of the awaiter.
co_await promise.initial_suspend()表达式的结果将会被丢弃,因此实现通常应该从awaiter的await_resume()方法返回void。
It is important to note that this statement exists outside of the try
/catch
block that guards the rest of the coroutine (scroll back up to the definition of the coroutine body if you’ve forgotten what it looks like). This means that any exception thrown from the co_await promise.initial_suspend()
evaluation prior to hitting its <return-to-caller-or-resumer>
will be thrown back to the caller of the coroutine after destroying the coroutine frame and the return object.
需要注意的是,这条语句存在于守护着协程其余部分的try/catch块之外(如果你忘了coroutine主体的定义是什么样子的,请向上滚动到协程体的定义)。这意味着在达到<return-to-caller-or-resumer>之前从co_await promise.initial_suspend()方法中抛出的任何异常都会在销毁协程帧和返回对象后被抛回协程的主调函数。
Be aware of this if your return-object
has RAII semantics that destroy the coroutine frame on destruction. If this is the case then you want to make sure that co_await promise.initial_suspend()
is noexcept
to avoid double-free of the coroutine frame.
如果你的返回对象有RAII语义,在销毁时破坏了协程帧,请注意这一点。如果是这种情况,那么你要确保co_await promise.initial_suspend()是noexcept,以避免协程帧被二次释放。
Note that there is a proposal to tweak the semantics so that either all or part of the co_await promise.initial_suspend()
expression lies inside try/catch block of the coroutine-body so the exact semantics here are likely to change before coroutines are finalised.
请注意,有人提议调整语义,使co_await promise.initial_suspend()表达式的全部或部分位于协程函数体的try/catch块内,因此在coroutines最终确定之前,这里的确切语义可能会发生变化。
For many types of coroutine, the initial_suspend()
method either returns std::experimental::suspend_always
(if the operation is lazily started) or std::experimental::suspend_never
(if the operation is eagerly started) which are both noexcept awaitables so this is usually not an issue.
对于许多类型的协程,initial_suspend()方法要么返回std::experimental::suspend_always(如果操作是延迟地启动的),要么返回std::experimental::suspend_never(如果操作是急切地启动的),它们都是noexcept awaitables,所以这通常不是一个问题。
Returning to the caller
When the coroutine function reaches its first <return-to-caller-or-resumer>
point (or if no such point is reached then when execution of the coroutine runs to completion) then the return-object
returned from the get_return_object()
call is returned to the caller of the coroutine.
当协程函数到达它的第一个<return-to-caller-or-resumer>点(或者没有到达这个点,协程已经执行完成),那么从get_return_object()调用返回的return-object将返回给协程的主调函数。
Note that the type of the return-object
doesn’t need to be the same type as the return-type of the coroutine function. An implicit conversion from the return-object
to the return-type of the coroutine is performed if necessary.
请注意,return-object的类型不需要和协程函数的返回类型相同。如果有必要,将执行从return-object到协程返回类型的隐式转换。
Note that Clang’s implementation of coroutines (as of 5.0) defers executing this conversion until the return-object is returned from the coroutine call, whereas MSVC’s implementation as of 2017 Update 3 performs the conversion immediately after calling get_return_object()
. Although the Coroutines TS is not explicit on the intended behaviour, I believe MSVC has plans to change their implementation to behave more like Clang’s as this enables some interesting use cases.
请注意,Clang的coroutines实现(从5.0开始)推迟执行这种转换,直到从协程调用返回对象,而MSVC的实现从2017年Update 3开始在调用get_return_object()后立即执行转换。虽然Coroutines TS没有明确说明预期的行为,但我相信MSVC已经计划改变他们的实现,使其行为更像Clang的,因为这可以实现一些有趣的用例。
Returning from the coroutine using co_return
When the coroutine reaches a co_return
statement, it is translated into either a call to promise.return_void()
or promise.return_value(<expr>)
followed by a goto FinalSuspend
;.
当协程到达一个co_return语句时,它被翻译成对promise.return_void()或promise.return_value(<expr>)的调用,然后是一个goto FinalSuspend;。
The rules for the translation are as follows:
翻译的规则如下:
co_return
;
->promise.return_void();
co_return <expr>;
-><expr>; promise.return_void();
if<expr>
has typevoid
->promise.return_value(<expr>);
if<expr>
does not have typevoid
The subsequent goto FinalSuspend
; causes all local variables with automatic storage duration to be destructed in reverse order of construction before then evaluating co_await promise.final_suspend();
.
随后的goto FinalSuspend;会使所有具有自动存储周期的局部变量按照与构造顺序相反的顺序被析构,然后再执行co_await promise.final_suspend();。
Note that if execution runs off the end of a coroutine without a co_return
statement then this is equivalent to having a co_return;
at the end of the function body. In this case, if the promise_type
does not have a return_void()
method then the behaviour is undefined.
请注意,如果执行到一个没有co_return语句的协程尾部,那么这就相当于在函数体的末端有一个co_return;。在这种情况下,如果promise_type没有return_void()方法,那么该行为是未定义的。
If either the evaluation of <expr>
or the call to promise.return_void()
or promise.return_value()
throws an exception then the exception still propagates to promise.unhandled_exception()
(see below).
如果<expr>的执行或对promise.return_void()或promise.return_value()的调用抛出了一个异常,那么该异常仍然会传播到promise.unhandled_exception()(见下文)。
Handling exceptions that propagate out of the coroutine body
If an exception propagates out of the coroutine body then the exception is caught and the promise.unhandled_exception()
method is called inside the catch
block.
如果一个异常传出了协程体,那么这个异常就会被捕获,并在catch块中调用promise.unhandled_exception()方法。
Implementations of this method typically call std::current_exception()
to capture a copy of the exception to store it away to be later rethrown in a different context.
这个方法的实现通常会调用std::current_exception()来捕获异常的副本,并将其存储起来,以便以后在不同的上下文中重新抛出。
Alternatively, the implementation could immediately rethrow the exception by executing a throw;
statement. For example see folly::Optional However, doing so will (likely - see below) cause the the coroutine frame to be immediately destroyed and for the exception to propagate out to the caller/resumer. This could cause problems for some abstractions that assume/require the call to coroutine_handle::resume()
to be noexcept
, so you should generally only use this approach when you have full control over who/what calls resume()
.
另外,(Coroutine的具体)实现也可以通过执行throw;语句立即重新抛出异常。例如folly::Optional。然而,这样做会(很可能–见下文)导致协程帧立即被销毁,并且异常会传播到主调函数/resumer那里。这可能会给一些假设/要求调用coroutine_handle::resume()为noexcept的抽象带来问题,所以一般来说,你应该只在完全了解谁/什么函数调用resume()时使用这种方法。
Note that the current Coroutines TS wording is a little unclear on the intended behaviour if the call to unhandled_exception()
rethrows the exception (or for that matter if any of the logic outside of the try-block throws an exception).
请注意,如果调用unhandled_exception()重新抛出异常(或者说,如果try-block之外的任何逻辑抛出异常),应该怎么做目前Coroutines TS的措辞有点不明确。
My current interpretation of the wording is that if control exits the coroutine-body, either via exception propagating out of co_await promise.initial_suspend()
, promise.unhandled_exception()
or co_await promise.final_suspend()
or by the coroutine running to completion by co_await p.final_suspend()
completing synchronously then the coroutine frame is automatically destroyed before execution returns to the caller/resumer. However, this interpretation has its own issues.
我目前对措辞的解释是,如果控制权退出了coroutine-body,无论是通过从co_await promise.initial_suspend()、promise.unhandled_exception()或co_await promise.final_suspend()传播出来的异常,还是通过co_await p.final_suspend()同步完成的协程运行完成,那么协程帧在执行返回给主调函数/reumer之前会自动销毁。然而,这种解释有其自身的问题。
A future version of the Coroutines specification will hopefully clarify the situation. However, until then I’d stay away from throwing exceptions out of initial_suspend()
, final_suspend()
or unhandled_exception()
. Stay tuned!
Coroutines规范的未来版本将有望澄清这一情况。然而,在那之前,我会远离从initial_suspend()、final_suspend()或unhandled_exception()中抛出的异常。请继续关注!
The final-suspend point
Once execution exits the user-defined part of the coroutine body and the result has been captured via a call to return_void()
, return_value()
or unhandled_exception()
and any local variables have been destructed, the coroutine has an opportunity to execute some additional logic before execution is returned back to the caller/resumer.
一旦执行流退出协程体的用户定义部分,并且通过调用return_void()、return_value()或unhandled_exception()来捕获结果,以及所有局部变量被析构,协程有机会在执行返回给主调函数/resumer之前执行一些额外的逻辑。
The coroutine executes the co_await promise.final_suspend();
statement.
协程执行co_await promise.final_suspend();语句。
This allows the coroutine to execute some logic, such as publishing a result, signalling completion or resuming a continuation. It also allows the coroutine to optionally suspend immediately before execution of the coroutine runs to completion and the coroutine frame is destroyed.
这允许协程执行一些逻辑,比如发布一个结果、发出完成信号或恢复一个延续点。它还允许协程在协程执行完成和协程帧被销毁之前,有选择地立即暂停。
Note that it is undefined behaviour to resume()
a coroutine that is suspended at the final_suspend
point. The only thing you can do with a coroutine suspended here is destroy()
it.
请注意,resume()一个在final_suspend点被挂起的协程是未定义的行为。你能对在此挂起的程序做的唯一事情就是destroy()它。
The rationale for this limitation, according to Gor Nishanov, is that this provides several optimisation opportunities for the compiler due to the reduction in the number of suspend states that need to be represented by the coroutine and a potential reduction in the number of branches required.
根据Gor Nishanov的说法,这种限制的理由是,这减少了需要记录的协程挂起状态的数量,也可能减少了所需的分支数量,这为编译器提供了一些优化的机会。
Note that while it is allowed to have a coroutine not suspend at the final_suspend
point, it is recommended that you structure your coroutines so that they do suspend at final_suspend
where possible. This is because this forces you to call .destroy()
on the coroutine from outside of the coroutine (typically from some RAII object destructor) and this makes it much easier for the compiler to determine when the scope of the lifetime of the coroutine-frame is nested inside the caller. This in turn makes it much more likely that the compiler can elide the memory allocation of the coroutine frame.
请注意,虽然可以让一个协程不在final_suspend点暂停,但我们建议你的协程尽可能在final_suspend点暂停。这是因为这将迫使你从协程外部调用.destroy()(通常是从一些RAII对象的析构器),这使得编译器更容易确定协程帧的生命周期范围何时嵌套在主调函数内部。这反过来又使得编译器更有可能避免对协程帧的内存分配。
How the compiler chooses the promise type
So lets look now at how the compiler determines what type of promise object to use for a given coroutine.
所以现在让我们看看编译器是如何确定在给定的协程中使用什么类型的promise对象的。
The type of the promise object is determined from the signature of the coroutine by using the std::experimental::coroutine_traits
class.
promise对象的类型是通过使用std::experimental::coroutine_traits类从协程的签名中确定的。
If you have a coroutine function with signature:
Then the compiler will deduce the type of the coroutine’s promise by passing the return-type and parameter types as template arguments to coroutine_traits
.
然后,编译器将通过把返回类型和参数类型作为模板参数传递给coroutine_traits来推断coroutine的promise类型。
If the function is a non-static member function then the class type is passed as the second template parameter to coroutine_traits
. Note that if your method is overloaded for rvalue-references then the second template parameter will be an rvalue reference.
如果该函数是一个非静态成员函数,那么类的类型将作为第二个模板参数传递给coroutine_traits。注意,如果你的方法是为右值引用(参数类型)重载的,那么第二个模板参数将是一个右值引用。
For example, if you have the following methods:
例如,如果你有以下方法:
Then the compiler will use the following promise types:
那么编译器将使用以下promise类型:
The default definition of coroutine_traits
template defines the promise_type
by looking for a nested promise_type
typedef defined on the return-type. ie. Something like this (but with some extra SFINAE magic so that promise_type
is not defined if RET::promise_type
is not defined).
coroutine_traits模板的默认定义是通过寻找定义在返回类型上的嵌套promise_type类型来定义promise_type。 即类似这样的东西(但有一些额外的SFINAE技巧,如果RET::promise_type没有被定义,则promise_type不会被定义)。
So for coroutine return-types that you have control over, you can just define a nested promise_type
in your class to have the compiler use that type as the type of the promise object for coroutines that return your class.
因此,对于你可以控制的协程返回类型,你可以在你的类中定义一个嵌套的promise_type,让编译器将该类型作为promise对象的类型,该promise对象的协程返回你写的类。
For example:
However, for coroutine return-types that you don’t have control over you can specialise the coroutine_traits
to define the promise type to use without needing to modify the type.
然而,对于你无法控制的coroutine返回类型,你可以将coroutine_traits特殊化,以定义使用的promise类型,而不需要修改类型。
For example, to define the promise-type to use for a coroutine that returns std::optional<T>
:
例如,定义一个返回std::optional的协程所使用的promise类型。
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.
Coroutines TS为此提供的机制是coroutine_handle
类型。
The (abbreviated) interface of this type is as follows:
这种类型的(简写)接口如下:
You can obtain a coroutine_handle for a coroutine in two ways:
你可以通过两种方式为一个协程获得一个coroutine_handle:
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()
.
1.在co_await表达式中,它被传递给await_suspend()方法。
2.如果你有一个对协程promise对象的引用,你可以使用coroutine_handle<\Promise>::from_promise()重新构造它的coroutine_handle。
The coroutine_handle
of the awaiting coroutine will be passed into the await_suspend()
method of the awaiter after the coroutine has suspended at the <suspend-point>
of a co_await
expression. You can think of this coroutine_handle
as representing the continuation of the coroutine in a continuation-passing style call.
在 coroutine 在 co_await 表达式的 <suspend-point> 处暂停后,等待中的 coroutine_handle 将被传递到 awaiter 的 await_suspend() 方法中。你可以把这个coroutine_handle看作是在一个延续传递式的调用中协程的延续。
Note that the coroutine_handle
is NOT and RAII object. You must manually call .destroy()
to destroy the coroutine frame and free its resources. Think of it as the equivalent of a void*
used to manage memory. This is for performance reasons: making it an RAII object would add additional overhead to coroutine, such as the need for reference counting.
注意,coroutine_handle不是RAII对象。你必须手动调用.destroy()来销毁协程帧并释放其资源。可以把它看作是用于管理内存的void*的等价物。这是为了性能的原因:使它成为一个RAII对象会给coroutine增加额外的开销,例如需要引用计数。
You should generally try to use higher-level types that provide the RAII semantics for coroutines, such as those provided by cppcoro (shameless plug), or write your own higher-level types that encapsulate the lifetime of the coroutine frame for your coroutine type.
一般来说,你应该尽量使用为协程提供RAII语义的高级类型,比如那些由cppcoro提供的类型(在这打个广告),或者编写你自己的高级类型,为你的协程类型封装协程帧的生命周期。
Customising the behaviour of co_await
The promise type can optionally customise the behaviour of every co_await
expression that appears in the body of the coroutine.
promise类型可以选择性地定制出现在协程体中的每个co_await表达式的行为。
By simply defining a method named await_transform()
on the promise type, the compiler will then transform every co_await <expr>
appearing in the body of the coroutine into co_await promise.await_transform(<expr>)
.
通过简单地在 promise 类型上定义一个名为 await_transform() 的方法,编译器将把出现在 coroutine 主体中的每个 co_await <expr> 转变为 co_await promise.await_transform(<expr>) 。
This has a number of important and powerful uses:
这有许多重要而强大的用途。
It lets you enable awaiting types that would not normally be awaitable.
它可以让你能够等待通常不可以等待的(awaitable)类型。
For example, a promise type for coroutines with a std::optional<T>
return-type might provide an await_transform()
overload that takes a std::optional<U>
and that returns an awaitable type that either returns a value of type U or suspends the coroutine if the awaited value contains nullopt
.
例如,用于具有 std::optional<T> 返回类型的协程的 promise 类型可以提供一个 await_transform() 重载,它接受一个 std::optional<U> 并返回一个awaitable类型,该类型要么返回一个 U 类型的值,要么在等待的值包含 nullopt 时挂起协程。
It lets you disallow awaiting on certain types by declaring await_transform
overloads as deleted.
它允许你通过将 await_transform 重载声明为delete,来禁止在某些类型上进行等待。
For example, a promise type for std::generator<T>
return-type might declare a deleted await_transform()
template member function that accepts any type. This basically disables use of co_await
within the coroutine.
例如,一个用于std::generator<T>返回类型的承诺类型可以声明一个删除的await_transform()模板成员函数,它接受任何类型。这基本上禁止了coroutine中co_await的使用。
It lets you adapt and change the behaviour of normally awaitable values
它允许你调整和改变通常awaitable values的行为
For example, you could define a type of coroutine that ensured that the coroutine always resumed from every co_await
expression on an associated executor by wrapping the awaitable in a resume_on()
operator (see cppcoro::resume_on()
).
例如,你可以定义一种协程,确保协程总是从相关executor上的每个co_await表达式中恢复,方法是将awaitable包装在resume_on()操作符中(参见cppcoro::resume_on())。
As a final word on await_transform()
, it’s important to note that if the promise type defines any await_transform()
members then this triggers the compiler to transform all co_await
expressions to call promise.await_transform()
. This means that if you want to customise the behaviour of co_await
for just some types that you also need to provide a fallback overload of await_transform()
that just forwards through the argument.
关于 await_transform() 的最后一句话,重要的是要注意,如果promise类型定义了任何 await_transform() 成员,那么这将触发编译器将所有 co_await 表达式转换为调用 promise.await_transform() 。这意味着,如果你想为某些类型定制co_await的行为,你也需要提供一个await_transform()的回退重载,它只是转发参数。
Customising the behaviour of co_yield
The final thing you can customise through the promise type is the behaviour of the co_yield
keyword.
你可以通过promise类型定制的最后一件事是co_yield关键字的行为。
If the co_yield
keyword appears in a coroutine then the compiler translates the expression co_yield <expr>
into the expression co_await promise.yield_value(<expr>)
. The promise type can therefore customise the behaviour of the co_yield
keyword by defining one or more yield_value()
methods on the promise object.
如果co_yield关键字出现在一个协程中,那么编译器会将表达式co_yield <expr>翻译成表达式co_await promise.yield_value(<expr>)。因此,承诺类型可以通过在承诺对象上定义一个或多个 yield_value() 方法来定制 co_yield 关键字的行为。
Note that, unlike await_transform
, there is no default behaviour of co_yield
if the promise type does not define the yield_value()
method. So while a promise type needs to explicitly opt-out of allowing co_await
by declaring a deleted await_transform()
, a promise type needs to opt-in to supporting co_yield
.
注意,与 await_transform 不同,如果 promise 类型没有定义 yield_value() 方法,co_yield 没有默认行为。因此,虽然一个promise类型需要通过声明删除 await_transform() 明确地选择不允许 co_await,但一个promise类型需要选择支持 co_yield。
The typical example of a promise type with a yield_value()
method is that of a generator<T>
type:
带有 yield_value() 方法的 promise 类型的典型例子是 generator 类型。
Summary
In this post I’ve covered the individual transformations that the compiler applies to a function when compiling it as a coroutine.
在这篇文章中,我已经介绍了编译器在将一个函数编译为coroutine时对其进行的个别转换。
Hopefully this post will help you to understand how you can customise the behaviour of different types of coroutines through defining different your own promise type. There are a lot of moving parts in the coroutine mechanics and so there are lots of different ways that you can customise their behaviour.
希望这篇文章能帮助你了解如何通过定义不同的promise类型来定制不同类型的协程的行为。在协程机制中,有很多移动的部分,因此有很多不同的方法可以定制它们的行为。
However, there is still one more important transformation that the compiler performs which I have not yet covered - the transformation of the coroutine body into a state-machine. However, this post is already too long so I will defer explaining this to the next post. Stay tuned!
然而,编译器还进行了一个重要的转换,我还没有讲到–将协程体转换为一个状态机。然而,这篇文章已经太长了,所以我将把这个问题推迟到下一篇文章中解释。敬请期待。