本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第二篇,原文内容在https://lewissbaker.github.io/2017/09/25/coroutine-theory

In the previous post on Coroutine Theory I described the high-level differences between functions and coroutines but without going into any detail on syntax and semantics of coroutines as described by the C++ Coroutines TS (N4680).

之前的一篇关于协程理论的文章中我介绍了函数和协程间高层次的区别,但是没有在语法和语义上深入任何细节,这些内容在C++ Coroutines TS(N4680)中都有描述。

The key new facility that the Coroutines TS adds to the C++ language is the ability to suspend a coroutine, allowing it to be later resumed. The mechanism the TS provides for doing this is via the new co_await operator.

Coroutines TS 中加入到C++语言中的新特性包括挂起一个协程,允许它之后被恢复。TS通过新加入的co_await运算符实现了这个机制。

Understanding how the co_await operator works can help to demystify the behaviour of coroutines and how they are suspended and resumed. In this post I will be explaining the mechanics of the co_await operator and introduce the related Awaitable and Awaiter type concepts.

理解co_await运算符的原理可以帮助我们理解协程的行为以及协程是怎么挂起和恢复的。在这篇文章中我将解释co_await运算符的机制,并将介绍相关的Awaitable和Awaiter的概念。

But before I dive into co_await I want to give a brief overview of the Coroutines TS to provide some context.

但在详细介绍co_await之前我想先大概介绍介绍一下Coroutines TS提供的内容来补充一些背景知识。

What does the Coroutines TS give us?

  • Three new language keywords: co_await, co_yield and co_return

  • Several new types in the std::experimental namespace:

    • coroutine_handle
    • coroutine_traits<Ts…>
    • suspend_always
    • suspend_never
  • A general mechanism that library writers can use to interact with coroutines and customise their behaviour.

  • A language facility that makes writing asynchronous code a whole lot easier!

  • 三个新的关键字:co_await,co_yield和co_return

  • 一些 std::experimental 命名空间中的新类型(在翻译时gcc已经将其纳入std命名空间):

    • coroutine_handle
    • coroutine_traits
    • suspend_always
    • suspend_never
  • 一种库作者可以用于和协程交互以及定制协程行为的通用机制

  • 一种使得编写异步代码容易很多的语言工具

The facilities the C++ Coroutines TS provides in the language can be thought of as a low-level assembly-language for coroutines. These facilities can be difficult to use directly in a safe way and are mainly intended to be used by library-writers to build higher-level abstractions that application developers can work with safely.

C++ Coroutines TS所提供的语言特性可以认为是一种低级的汇编级语言协程。这些特性很难直接安全地应用,它们的目的主要是为了库作者能够用来构建高层级的抽象,这样应用开发者就可以安全地应用它们。

The plan is to deliver these new low-level facilities into an upcoming language standard (hopefully C++20) along with some accompanying higher-level types in the standard library that wrap these low-level building-blocks and make coroutines more accessible in a safe way for application developers.

计划是将这些新的低层特性加入到即将到来的语言标准(希望C++ 20)中,以及一些标准库的高级类型中(译注:如excutor),这些高级类型封装这些低级构建块,并使应用程序开发人员能够以安全的方式使用协程。

Compiler <-> Library interaction

Interestingly, the Coroutines TS does not actually define the semantics of a coroutine. It does not define how to produce the value returned to the caller. It does not define what to do with the return value passed to the co_return statement or how to handle an exception that propagates out of the coroutine. It does not define what thread the coroutine should be resumed on.

有意思的是,Coroutine TS中并没有直接给出协程的语义。它没有定义如何产生返回给主调函数的值。它没有定义将返回值传递给co_return语句的时候做什么或者怎么处理协程的异常传播。它没有定义协程应该在哪一个线程中恢复。

Instead, it specifies a general mechanism for library code to customise the behaviour of the coroutine by implementing types that conform to a specific interface. The compiler then generates code that calls methods on instances of types provided by the library. This approach is similar to the way that a library-writer can customise the behaviour of a range-based for-loop by defining the begin()/end() methods and an iterator type.

相反,它为库代码指定了一种通用机制,通过实现符合特定接口的类型来定制协程的行为。然后编译器生成代码,调用库提供的类型实例方法。这种方法类似于库作者通过定义begin()/end()方法和迭代器类型来定制基于范围的for循环的行为。

The fact that the Coroutines TS doesn’t prescribe any particular semantics to the mechanics of a coroutine makes it a powerful tool. It allows library writers to define many different kinds of coroutines, for all sorts of different purposes.

事实上,Coroutine TS没有为协程的机制规定任何特定的语义,这使得它成为一个强大的工具。它允许库作者为各种不同的目的定义许多不同种类的协程程序。

For example, you can define a coroutine that produces a single value asynchronously, or a coroutine that produces a sequence of values lazily, or a coroutine that simplifies control-flow for consuming optional<T> values by early-exiting if a nullopt value is encountered.

例如,你可以定义一个异步产生单个值的协程,或者一个延迟产生一系列值的协程,或者一个在遇到nullopt值时通过提前退出来简化消耗optional值的控制流的协程。

There are two kinds of interfaces that are defined by the coroutines TS: The Promise interface and the Awaitable interface.

Coroutines TS中定义了两种接口:Promise接口和Awaitable接口。

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时的行为。

The Awaitable interface specifies methods that control the semantics of a co_await expression. When a value is co_awaited, the code is translated into a series of calls to methods on the awaitable object that allow it to specify: whether to suspend the current coroutine, execute some logic after it has suspended to schedule the coroutine for later resumption, and execute some logic after the coroutine resumes to produce the result of the co_await expression.

Awaitable接口指定了控制co_wait表达式语义的方法。当一个值被co_awaited时,代码被转换成一系列对awaitiable对象上的方法的调用,这些调用允许它指定:是否挂起当前协程,在它挂起后执行一些逻辑来调度(/安排)协程以便稍后恢复,并在协程恢复后执行一些逻辑,以生成co_await表达式的结果。

I’ll be covering details of the Promise interface in a future post, but for now let’s look at the Awaitable interface.

我将在之后的一篇文章中介绍Promise的更多细节,但现在让我们首先聚焦于Awaitable接口。

Awaiters and Awaitables: Explaining operator co_await

The co_await operator is a new unary operator that can be applied to a value. For example: co_await someValue.

co_await运算符是一个可以被应用于某个值的新的一元运算符。例如:co_await someValue。

The co_await operator can only be used within the context of a coroutine. This is somewhat of a tautology though, since any function body containing use of the co_await operator, by definition, will be compiled as a coroutine.

co_await操作符只能在协程的上下文中使用。不过,这有点像重言式,因为根据定义,任何包含使用co_await运算符的函数体都将被编译为一个协程。

A type that supports the co_await operator is called an Awaitable type.

支持co_await运算符的类型被称为Awaitable类型。

Note that whether or not the co_await operator can be applied to a type can depend on the context in which the co_await expression appears. The promise type used for a coroutine can alter the meaning of a co_await expression within the coroutine via its await_transform method (more on this later).

请注意,co_await运算符是否可以应用于类型取决于co_await表达式出现的上下文环境。协程的promise类型可以通过其await_transform方法(稍后将详细介绍)更改协程中co_await表达式的含义。

To be more specific where required I like to use the term Normally Awaitable to describe a type that supports the co_await operator in a coroutine context whose promise type does not have an await_transform member. And I like to use the term Contextually Awaitable to describe a type that only supports the co_await operator in the context of certain types of coroutines due to the presence of an await_transform method in the coroutine’s promise type. (I’m open to better suggestions for these names here…)

更具体地说,在需要时,我喜欢使用术语normaly Awaitable来描述一个类型,该类型在协程上下文中支持co_await操作符,其promise类型没有await_transform成员。我喜欢使用术语Contextual Awaitable来描述另外一种类型,这种类型在某些类型的协程的上下文中只支持co_await操作符,因为在协程的promise类型中存在await_transform转换方法。(若对这些概念命名有更好的建议,我很乐意接受……)

An Awaiter type is a type that implements the three special methods that are called as part of a co_await expression: await_ready, await_suspend and await_resume.

Awaiter类型是一种实现了co_await表达式中三个特殊方法的类型,这三个方法是:await_ready、await_suspend和await_resume。

Note that I have shamelessly “borrowed” the term ‘Awaiter’ here from the C# async keyword’s mechanics that is implemented in terms of a GetAwaiter() method which returns an object with an interface that is eerily similar to the C++ concept of an Awaiter. See this post for more details on C# awaiters.

这里我从C# async关键字的实现机制中的GetAwaiter() 方法中借鉴了Awaiter概念,getAwaiter方法返回的对象实现的接口十分类似于C++中Awaiter的概念。可以阅读这篇文章以了解更多C#的Awaiter。

Note that a type can be both an Awaitable type and an Awaiter type.

注意一个类型可以既是Awaitable类型也是Awaiter类型。

When the compiler sees a co_await <expr> expression there are actually a number of possible things it could be translated to depending on the types involved.

当编译器遇到一个co_await <expr> 表达式时,根据涉及到的类型它可能会将其翻译成不同的东西。

Obtaining the Awaiter

The first thing the compiler does is generate code to obtain the Awaiter object for the awaited value. There are a number of steps to obtaining the awaiter object which are set out in N4680 section 5.3.8(3).

编译器要做的第一件事是为要等待的值产生Awaiter对象的代码。N4680第5.3.8(3) 节规定了获取awaiter对象的若干步骤。

Let’s assume that the promise object for the awaiting coroutine has type, P, and that promise is an l-value reference to the promise object for the current coroutine.

让我们假设正在等待的协程的promise类型为P,且该promise类型是当前(执行着的被等待的)协程promise类型的一个左值引用。

If the promise type, P, has a member named await_transform then <expr> is first passed into a call to promise.await_transform(<expr>) to obtain the Awaitable value, awaitable. Otherwise, if the promise type does not have an await_transform member then we use the result of evaluating <expr> directly as the Awaitable object, awaitable.

如果promise类型P存在await_transform成员,那么<expr>将会首先被传给promise.await_transform(<expr>)来获取Awaitable值——awaitable。反之,如果promise类型没有await_transform成员,那么就会直接使用<expr>的结果作为Awaitable对象——awaitable。

Then, if the Awaitable object, awaitable, has an applicable operator co_await() overload then this is called to obtain the Awaiter object. Otherwise the object, awaitable, is used as the awaiter object.

然后,如果Awaitable对象——awaitable,有可用的(重载)co_await运算符,那么就调用co_await来获取Awaiter对象。否则,就直接将awaitable作为awaiter对象。

If we were to encode these rules into the functions get_awaitable() and get_awaiter(), they might look something like this:

若我们将上述规则编码成函数get_awaitable()和get_awaiter(),那么它可能会如下所示:

Awaiting the Awaiter

So, assuming we have encapsulated the logic for turning the <expr> result into an Awaiter object into the above functions then the semantics of co_await <expr> can be translated (roughly) as follows:

所以,假设我们已经封装了将<expr>结果转换成Awaiter对象的逻辑,那么co_await <expr>的语义(大致上)可以翻译为:

The void-returning version of await_suspend() unconditionally transfers execution back to the caller/resumer of the coroutine when the call to await_suspend() returns, whereas the bool-returning version allows the awaiter object to conditionally resume the coroutine immediately without returning to the caller/resumer.

对于返回值为void版本的await_suspend,当对await_suspend的调用返回时无条件转回执行协程的主调函数/调用resume者。而返回值为bool类型的版本则允许awaiter对象有条件地不返回主调函数/resumer而立即恢复协程。

The bool-returning version of await_suspend() can be useful in cases where the awaiter might start an async operation that can sometimes complete synchronously. In the cases where it completes synchronously, the await_suspend() method can return false to indicate that the coroutine should be immediately resumed and continue execution.

返回值为bool类型的版本的await_suspend在awaiter启动一个异步操作但有时该操作可以同步地完成的时候会很有用。当某些情况下的操作同步地完成后,await_suspend方法可以返回false来指示协程立即恢复和继续执行。

At the <suspend-coroutine> point the compiler generates some code to save the current state of the coroutine and prepare it for resumption. This includes storing the location of the <resume-point> as well as spilling any values currently held in registers into the coroutine frame memory.

在协程挂起点编译器产生一些代码用于保存协程当前的状态以备于之后恢复。这包括存储恢复点的位置以及将当前寄存器值保存到协程帧中。

The current coroutine is considered suspended after the <suspend-coroutine> operation completes. The first point at which you can observe the suspended coroutine is inside the call to await_suspend(). Once the coroutine is suspended it is then able to be resumed or destroyed.

在挂起协程的操作完成后,当前协程可以被认为暂停了。你第一个可以观察到挂起的协程的位置在await_suspend调用的内部。一旦协程挂起,那么它可以被恢复或被销毁。

It is the responsibility of the await_suspend() method to schedule the coroutine for resumption (or destruction) at some point in the future once the operation has completed. Note that returning false from await_suspend() counts as scheduling the coroutine for immediate resumption on the current thread.

如果操作完成后,将由await_suspend方法负责安排在未来某个位置恢复或销毁协程。当await_suspend返回false时意味着将在当前线程中立即将协程安排为恢复执行。

The purpose of the await_ready() method is to allow you to avoid the cost of the <suspend-coroutine> operation in cases where it is known that the operation will complete synchronously without needing to suspend.

await_ready方法允许你在知道操作将会同步完成无需挂起时避免协程挂起操作。

At the <return-to-caller-or-resumer> point execution is transferred back to the caller or resumer, popping the local stack frame but keeping the coroutine frame alive.

<return-to-caller-or-resumer> 位置协程将会将执行权交回给主调函数或恢复者,将栈帧出栈,但会保存协程帧。

When (or if) the suspended coroutine is eventually resumed then the execution resumes at the <resume-point>. ie. immediately before the await_resume() method is called to obtain the result of the operation.

当挂起的协程被恢复时,协程会在resume-point恢复执行,该位置也是调用await_resume方法获得操作的结果(紧挨着)之前。

The return-value of the await_resume() method call becomes the result of the co_await expression. The await_resume() method can also throw an exception in which case the exception propagates out of the co_await expression.

await_resume方法调用的返回值将会成为co_await表达式的结果。当异常传播出co_await表达式时,await_resume方法也可以抛出一个异常。

Note that if an exception propagates out of the await_suspend() call then the coroutine is automatically resumed and the exception propagates out of the co_await expression without calling await_resume().

注意当一个异常由await_suspend调用传播出来时,协程会自动恢复执行,这个异常将在不调用await_resume方法的情况下传播出co_await表达式。

Coroutine Handles

You may have noticed the use of the coroutine_handle<P> type that is passed to the await_suspend() call of a co_await expression.

你可能已经注意到coroutine_handle<P>类型已经被传给co_await表达式中的await_suspend调用。

This type represents a non-owning handle to the coroutine frame and can be used to resume execution of the coroutine or to destroy the coroutine frame. It can also be used to get access to the coroutine’s promise object.

这个类型代表了不包含协程帧所有权的句柄,它可以用于恢复协程的执行或者销毁协程帧。它也可以用于访问协程的promise对象。

The coroutine_handle type has the following (abbreviated) interface:

coroutine_handle类型有如下接口(简写):

When implementing Awaitable types, they key method you’ll be using on coroutine_handle will be .resume(), which should be called when the operation has completed and you want to resume execution of the awaiting coroutine. Calling .resume() on a coroutine_handle reactivates a suspended coroutine at the <resume-point>. The call to .resume() will return when the coroutine next hits a <return-to-caller-or-resumer> point.

当实现Awaitable类型时,你在coroutine_handle上应用的最主要的方法将是.resume方法,该方法将会在操作完成后你想要恢复等待着的协程时执行。在coroutine_handle上调用.resume方法将会在协程的resume-point重新激活协程。对协程的.resume的调用将会在协程下一次执行到<return-to-caller-or-resumer> point时返回。

The .destroy() method destroys the coroutine frame, calling the destructors of any in-scope variables and freeing memory used by the coroutine frame. You should generally not need to (and indeed should really avoid) calling .destroy() unless you are a library writer implementing the coroutine promise type. Normally, coroutine frames will be owned by some kind of RAII type returned from the call to the coroutine. So calling .destroy() without cooperation with the RAII object could lead to a double-destruction bug.

.destroy方法将会销毁协程帧,调用作用域内的变量的析构函数然后释放协程帧的内存。你一般不用手动调用.destroy方法,除非你是库作者。一般协程帧将会被一个RAII类型管理,所以调用.destroy方法可能会导致二次析构的bug。

The .promise() method returns a reference to the coroutine’s promise object. However, like .destroy(), it is generally only useful if you are authoring coroutine promise types. You should consider the coroutine’s promise object as an internal implementation detail of the coroutine. For most Normally Awaitable types you should use coroutine_handle<void> as the parameter type to the await_suspend() method instead of coroutine_handle<Promise>.

.promise方法会返回协程promise对象的一个引用。然而,就像.destroy方法一样,只有当你写协程promise类型时它才可能有用。你可以将协程的promise对象视为协程的一个内部实现细节。对于Normally Awaitable类型你应该使用coroutine_handle<void>作为await_suspend()的参数类型而不是用coroutine_handle<Promise>。

The coroutine_handle<P>::from_promise(P& promise) function allows reconstructing the coroutine handle from a reference to the coroutine’s promise object. Note that you must ensure that the type, P, exactly matches the concrete promise type used for the coroutine frame; attempting to construct a coroutine_handle<Base> when the concrete promise type is Derived can lead to undefined behaviour.

coroutine_handle<P>::from_promise(P& promise)函数允许你使用协程promise对象的引用重建一个协程句柄。需要注意的是你必须确保类型P能完全匹配用于协程帧的具体promise类型;当具体类型是Derived而意图去创建一个coroutine_handle<Base>时可能会导致未定义行为。

The .address() / from_address() functions allow converting a coroutine handle to/from a void* pointer. This is primarily intended to allow passing as a ‘context’ parameter into existing C-style APIs, so you might find it useful in implementing Awaitable types in some circumstances. However, in most cases I’ve found it necessary to pass additional information through to callbacks in this ‘context’ parameter so I generally end up storing the coroutine_handle in a struct and passing a pointer to the struct in the ‘context’ parameter rather than using the .address() return-value.

.address/from_address()函数允许将一个协程句柄转换为void *指针,或者将void *指针转化为一个协程句柄。这主要是为了可以将一个“上下文”参数传给已有的C风格的API,所以在某些情况下(自己)实现Awaitable类型是有用的。然而,大多数情况下我发现在传递“上下文”参数时往往也有将一些额外信息传给回调函数的需要,所以我一般会将coroutine_handle存储到一个结构体里然后传递这个结构体的指针而不是使用.address的返回值。

Synchronisation-free async code

One of the powerful design-features of the co_await operator is the ability to execute code after the coroutine has been suspended but before execution is returned to the caller/resumer.

co_await运算符的一个强大的特性是可以在协程挂起和执行权返回主调函数/resumer之前执行代码。

This allows an Awaiter object to initiate an async operation after the coroutine is already suspended, passing the coroutine_handle of the suspended coroutine to the operation which it can safely resume when the operation completes (potentially on another thread) without any additional synchronisation required.

这允许一个Awaiter对象在协程挂起后初始化一个异步操作,将挂起的协程的coroutine_handle传递给后续能安全恢复(挂起的协程)的操作,该操作完成时就可以恢复协程(可能是在另一个线程中),并且这当中不需要额外的同步语句。

For example, by starting an async-read operation inside await_suspend() when the coroutine is already suspended means that we can just resume the coroutine when the operation completes without needing any thread-synchronisation to coordinate the thread that started the operation and the thread that completed the operation.

例如,当协程挂起后可以在await_suspend里开启一个异步读的操作,这样当(读)操作完成后我们就可以恢复协程,这当中不需要线程同步语句来协调开启操作的线程和完成操作的线程。

One thing to be very careful of when taking advantage of this approach is that as soon as you have started the operation which publishes the coroutine handle to other threads then another thread may resume the coroutine on another thread before await_suspend() returns and may continue executing concurrently with the rest of the await_suspend() method.

你要非常注意的一点是当你使用这个方法时一旦你开启了将协程句柄发布给其它线程的操作,那么其它线程可能会在await_suspend方法返回之前恢复协程,这可能会导致(其它线程中的)协程与(当前线程中的协程的)await_suspend方法的剩余部分并发地执行。

The first thing the coroutine will do when it resumes is call await_resume() to get the result and then often it will immediately destruct the Awaiter object (ie. the this pointer of the await_suspend() call). The coroutine could then potentially run to completion, destructing the coroutine and promise object, all before await_suspend() returns.

协程返回时第一件要做的事就是调用await_resume方法来获取结果,之后,它大概率会立即销毁Awaiter对象(也就是await_suspend调用的this指针)。协程可能接下来会执行到结束,销毁协程和promise对象,这一切都是在(当前线程中的)await_suspend返回之前完成的。

So within the await_suspend() method, once it’s possible for the coroutine to be resumed concurrently on another thread, you need to make sure that you avoid accessing this or the coroutine’s .promise() object because both could already be destroyed. In general, the only things that are safe to access after the operation is started and the coroutine is scheduled for resumption are local variables within await_suspend().

所以,一旦有可能在另一个线程中并发恢复当前协程,你需要保证(在await_suspend方法中)避免访问this指针或协程的promise对象,因为这些可能已经(在另外的线程中)被销毁。一般意义上,在操作开始和协程被安排在未来某点恢复之后能安全访问的内容就是await_suspend内的局部变量。

Comparison to Stackful Coroutines

I want to take a quick detour to compare this ability of the Coroutines TS stackless coroutines to execute logic after the coroutine is suspended with some existing common stackful coroutine facilities such as Win32 fibers or boost::context.

在这我们提点题外话,稍微对比一下Coroutines TS的无栈协程和其它有栈协程像Win32 fibers或boost::context在协程挂起之后执行额外逻辑的能力。

With many of the stackful coroutine frameworks, the suspend operation of a coroutine is combined with the resumption of another coroutine into a ‘context-switch’ operation. With this ‘context-switch’ operation there is typically no opportunity to execute logic after suspending the current coroutine but before transferring execution to another coroutine.

在许多有栈协程框架中,协程的挂起操作和另一个协程的恢复操作被整合成一个“上下文切换”操作。对于“上下文切换”操作,程序就没有在当前协程挂起后与其它协程执行之前执行额外逻辑的机会。

This means that if we want to implement a similar async-file-read operation on top of stackful coroutines then we have to start the operation before suspending the coroutine. It is therefore possible that the operation could complete on another thread before the coroutine is suspended and is eligible for resumption. This potential race between the operation completing on another thread and the coroutine suspending requires some kind of thread synchronisation to arbitrate and decide on the winner.

这意味着如果我们想利用有栈协程实现一个类似于异步读文件的操作,那么我们就必须在协程挂起之前开启它。那么就有可能出现异步读操作在当前协程未挂起之前在另一个线程中已经完成,此时恢复(当前协程)的条件已经满足。在另一个线程上完成的操作和协程挂起之间的这种潜在竞态条件需要某些线程同步(语句)来仲裁来决定谁先执行。

There are probably ways around this by using a trampoline context that can start the operation on behalf of the initiating context after the initiating context has been suspended. However this would require extra infrastructure and an extra context-switch to make it work and it’s possible that the overhead this introduces would be greater than the cost of the synchronisation it’s trying to avoid.

有一种解决方法是可以在初始上下文挂起时使用蹦床上下文代表初始上下文来开启操作。然而这需要一些额外的(同步)设施和上下文切换,而这带来的代价可能比它所想要避免的同步代价还要大。

Avoiding memory allocations

Async operations often need to store some per-operation state that keeps track of the progress of the operation. This state typically needs to last for the duration of the operation and should only be freed once the operation has completed.

异步操作往往需要存储一些各步操作状态以跟踪操作的进度。这个状态需要持续整个操作的周期,其内存只能在操作完成后释放。

For example, calling async Win32 I/O functions requires you to allocate and pass a pointer to an OVERLAPPED structure. The caller is responsible for ensuring this pointer remains valid until the operation completes.

例如,调用Win32异步I/O函数需要你分配一块儿内存然后将其指针传给OVERLAPPED结构体。主调函数有义务确保这个指针在操作完成之前都有效。

With traditional callback-based APIs this state would typically need to be allocated on the heap to ensure it has the appropriate lifetime. If you were performing many operations, you may need to allocate and free this state for each operation. If performance is an issue then a custom allocator may be used that allocates these state objects from a pool.

在传统的基于回调的API中,这个状态需要被分配在堆上以确保它有合适的生命周期。如果你执行多个操作,你需要为每次操作都分配和回收状态。如果性能有要求的话,可能会采用内存池技术来分配这些状态对象。

However, when we are using coroutines we can avoid the need to heap-allocate storage for the operation state by taking advantage of the fact that local variables within the coroutine frame will be kept alive while the coroutine is suspended.

然而,当使用协程时我们可以避免操作状态的堆分配,因为当协程挂起时协程帧中的局部变量仍然会被保留。

By placing the per-operation state in the Awaiter object we can effectively “borrow” memory from the coroutine frame for storing the per-operation state for the duration of the co_await expression. Once the operation completes, the coroutine is resumed and the Awaiter object is destroyed, freeing that memory in the coroutine frame for use by other local variables.

通过将每步操作状态放置在Awaiter对象中我们可以有效地利用协程帧的内存来在co_await表达式期间存储每步状态。一旦操作完成,协程恢复,Awaiter对象被销毁,那么协程帧中的(关于操作的变量)内存就会被释放,以存储其它的局部变量。

Ultimately, the coroutine frame may still be allocated on the heap. However, once allocated, a coroutine frame can be used to execute many asynchronous operations with only that single heap allocation.

最终,协程帧可能仍然分配在堆上。然而,一旦被分配,一个协程帧可以只需要一次堆分配就可以用于执行多个异步操作。

If you think about it, the coroutine frame acts as a kind of really high-performance arena memory allocator. The compiler figures out at compile time the total arena size it needs for all local variables and is then able to allocate this memory out to local variables as required with zero overhead! Try beating that with a custom allocator ; )

如果你细想一下,协程帧的行为就像是一个高性能的arena内存分配器。编译器在编译期计算所有局部变量需要的全部arena内存大小,然后就可以零开销地为这些局部变量分配内存。可以试试看别的内存分配器性能能否超越它。

An example: Implementing a simple thread-synchronisation primitive

Now that we’ve covered a lot of the mechanics of the co_await operator, I want to show how to put some of this knowledge into practice by implementing a basic awaitable synchronisation primitive: An asynchronous manual-reset event.

我们已经讨论了co_await操作符的许多机制,接下来我想展示如何通过实现一个基本的等待同步原语(异步手动设置事件)来将这些知识付诸实践。

The basic requirements of this event is that it needs to be Awaitable by multiple concurrently executing coroutines and when awaited needs to suspend the awaiting coroutine until some thread calls the .set() method, at which point any awaiting coroutines are resumed. If some thread has already called .set() then the coroutine should continue without suspending.

此事件的基本要求是,它需要通过多个并发执行的协程等待,等待时需要挂起等待的协程,直到某个线程调用.set()方法,此时任何等待的协程都将恢复。如果已经有某个线程调用过.set方法,那么协程将会不挂起而继续执行。

Ideally we’d also like to make it noexcept, require no heap allocations and have a lock-free implementation.

理想情况下,我们也希望它不会抛出异常,不需要堆分配,并且有一个无锁实现。

Edit 2017/11/23: Added example usage for async_manual_reset_event

Example usage should look something like this:

例子应用如下:

Let’s first think about the possible states this event can be in: ‘not set’ and ‘set’.

让我们首先考虑这个事件可能处于的状态:“not set”和“set”。

When it’s in the ‘not set’ state there is a (possibly empty) list of waiting coroutines that are waiting for it to become ‘set’.

当它处于“not set”状态时,会有一个(可能是空的)等待协程列表,等待它变成“set”。

When it’s in the ‘set’ state there won’t be any waiting coroutines as coroutines that co_await the event in this state can continue without suspending.

当它处于“set”状态时,将不会有任何等待的协程,因为协程等待此状态下的事件可以继续而不挂起。

This state can actually be represented in a single std::atomic<void*>.

此状态实际上可以用std::atomic<void*>表示。

  • Reserve a special pointer value for the ‘set’ state. In this case we’ll use the this pointer of the event since we know that can’t be the same address as any of the list items.

  • Otherwise the event is in the ‘not set’ state and the value is a pointer to the head of a singly linked-list of awaiting coroutine structures.

  • 为“set”状态保留一个特殊指针值。在本例中,我们将使用事件的this指针,因为我们知道它不能与任何列表项的地址相同。

  • 否则,事件处于“not set”状态,并且该值是指向正在等待的协程的单链表的头结点的指针。

We can avoid extra calls to allocate nodes for the linked-list on the heap by storing the nodes within an ‘awaiter’ object that is placed within the coroutine frame.

我们可以将节点存储在协程帧的awaiter对象里,这样就可以避免在堆上为链表分配节点带来的额外的系统调用。

So let’s start with a class interface that looks something like this:

下面有一个类接口:

Here we have a fairly straight-forward and simple interface. The main thing to note at this point is that it has an operator co_await() method that returns an, as yet, undefined type, awaiter.

这是一个相当直接和简单的接口。此时需要注意的主要问题是,它有一个co_await()方法,该方法返回一个尚未定义的类型awaiter。

Let’s define the awaiter type now.

下面让我们定义awaiter类型。

Defining the Awaiter

Firstly, it needs to know which async_manual_reset_event object it is going to be awaiting, so it will need a reference to the event and a constructor to initialise it.

首先,它需要知道它将等待哪个async_manual_reset_event事件对象,因此它需要一个对事件的引用和一个构造函数来初始化它。

It also needs to act as a node in a linked-list of awaiter values so it will need to hold a pointer to the next awaiter object in the list.

它还需要充当awaiter值链表中的节点,因此需要持有指向链表中下一个awaiter对象的指针。

It also needs to store the coroutine_handle of the awaiting coroutine that is executing the co_await expression so that the event can resume the coroutine when it becomes ‘set’. We don’t care what the promise type of the coroutine is so we’ll just use a coroutine_handle<> (which is short-hand for coroutine_handle<void>).

它还需要存储正在执行co_await表达式的等待着的协程的coroutine_handle句柄,以便事件在变为“set”时可以恢复协程。我们不关心协程的promise类型,所以我们只使用协程句柄coroutine_handle<>(coroutine_handle<void>的缩写)。

Finally, it needs to implement the Awaiter interface, so it needs the three special methods: await_ready, await_suspend and await_resume. We don’t need to return a value from the co_await expression so await_resume can return void.

最后,它需要实现Awaiter接口,因此需要三种特殊的方法:await_ready、await_suspend和await_ resume。我们不需要从co_await表达式返回值,因此await_resume可以返回void。

Once we put all of that together, the basic class interface for awaiter looks like this:

一旦我们把所有这些放在一起,awaiter的基本类接口如下所示:

Now, when we co_await an event, we don’t want the awaiting coroutine to suspend if the event is already set. So we can define await_ready() to return true if the event is already set.

现在,当我们co_await一个事件时,如果事件已经set,我们不希望等待的协程挂起。因此,如果已经事件状态已经变为set,我们可以让await_ready()返回true。

Next, let’s look at the await_suspend() method. This is usually where most of the magic happens in an awaitable type.

接下来,让我们看看await_suspend()方法。这通常是awaitable类型的魔法发生的地方。

First it will need to stash the coroutine handle of the awaiting coroutine into the m_awaitingCoroutine member so that the event can later call .resume() on it.

首先,它需要将等待着的协程的协程句柄存储到m_awaitingCoroutine成员中,以便事件稍后可以对其调用.resume()。

Then once we’ve done that we need to try and atomically enqueue the awaiter onto the linked list of waiters. If we successfully enqueue it then we return true to indicate that we don’t want to resume the coroutine immediately, otherwise if we find that the event has concurrently been changed to the ‘set’ state then we return false to indicate that the coroutine should be resumed immediately.

一旦我们做了(上述操作),我们需要尝试将awaiter原子地进入到等待协程的链表。如果我们成功地将其入队,那么我们返回true以指示我们不希望立即恢复协程,否则如果我们发现事件已同时更改为“set”状态,那么我们返回false以指示应立即恢复协程。

Note that we use ‘acquire’ memory order when loading the old state so that if we read the special ‘set’ value then we have visibility of writes that occurred prior to the call to ‘set()’.

请注意,在加载旧状态时,我们使用“acquire”内存顺序,这样,如果读取特殊的“set”值,就可以看到在调用“set()”之前发生的写操作。

We require ‘release’ sematics if the compare-exchange succeeds so that a subsequent call to ‘set()’ will see our writes to m_awaitingCoroutine and prior writes to the coroutine state.

如果compare-exchange成功,则需要“release”语义的内存顺序,以便随后对“set()”的调用将看到我们对m_awaitingCoroutine的写入以及对协程状态之前的写入。

Filling out the rest of the event class

Now that we have defined the awaiter type, let’s go back and look at the implementation of the async_manual_reset_event methods.

既然我们已经定义了awaiter类型,那么我们回头看看async_manua_reset_event方法的实现。

First, the constructor. It needs to initialise to either the ‘not set’ state with the empty list of waiters (ie. nullptr) or initialise to the ‘set’ state (ie. this).

首先,构造器。它需要初始化为“not set”状态和空的等待者列表(即nullptr),或者初始化为“set”状态(即this)。

Next, the is_set() method is pretty straight-forward - it’s ‘set’ if it has the special value this:

接下来,is_set()方法非常简单,如果它有以下特殊值,则为“set”:

Next, the reset() method. If it’s in the ‘set’ state we want to transition back to the empty-list ‘not set’ state, otherwise leave it as it is.

接下来是reset()方法。如果它处于“set”状态,我们希望转换回空列表的“not set”状态,否则保持原样。

With the set() method, we want to transition to the ‘set’ state by exchanging the current state with the special ‘set’ value, this, and then examine what the old value was. If there were any waiting coroutines then we want to resume each of them sequentially in turn before returning.

使用set()方法,我们希望通过将当前状态与特殊的“set”值——“this”交换,从而转换到“set”状态,然后检查旧值是什么。如果有任何等待的协程,那么我们希望在返回之前依次恢复它们中的每一个。

Finally, we need to implement the operator co_await() method. This just needs to construct an awaiter object.

最后,我们需要实现操作符co_await()方法。这只需要构造一个awaiter对象。

And there we have it. An awaitable asynchronous manual-reset event that has a lock-free, memory-allocation-free, noexcept implementation.

然后我们就有了,可等待的异步手动重置事件,具有无锁、无内存分配、“noexcept”实现。

If you want to have a play with the code or check out what it compiles down to under MSVC and Clang have a look at the source on godbolt.

如果你想玩一下代码或者看看它在MSVC和Clang下编译成什么,可以看看godbolt上的源代码。

You can also find an implementation of this class available in the cppcoro library, along with a number of other useful awaitable types such as async_mutex and async_auto_reset_event.

你还可以在cppcoro库中找到此类的实现,以及其他一些有用的等待类型,如async_mutex和async_auto_reset_event。

Closing Off

This post has looked at how the operator co_await is implemented and defined in terms of the Awaitable and Awaiter concepts.

本篇文章介绍了如何根据Awaitable和Awaiter的概念来实现和定义co_await操作符。

It has also walked through how to implement an awaitable async thread-synchronisation primitive that takes advantage of the fact that awaiter objects are allocated on the coroutine frame to avoid additional heap allocations.

本文还介绍了如何实现可等待的异步线程同步原语,该原语利用了在协程帧上分配的awaiter对象,以避免额外的堆分配。

I hope this post has helped to demystify the new co_await operator for you.

我希望这篇文章有助于为你揭开新的co_await运算符的神秘面纱。

In the next post I’ll explore the Promise concept and how a coroutine-type author can customise the behaviour of their coroutine.

在下一篇文章里我将探讨Promise的概念,以及协程类型的作者如何定制他们的协程的行为。

Thanks

I want to call out special thanks to Gor Nishanov for patiently and enthusiastically answering my many questions on coroutines over the last couple of years.

我要特别感谢Gor Nishanov在过去几年中耐心而热情地回答了我关于协程的许多问题。

And also to Eric Niebler for reviewing and providing feedback on an early draft of this post.

同时也感谢Eric Niebler对本文初稿的审阅和反馈。

C++协程(二):Understanding operator co_await相关推荐

  1. C++协程(三):Understanding the promise type

    本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第三篇,原文内容在https://lewissbaker.github.io/2018/09/05/un ...

  2. [译]C++ 协程:理解 co_await 运算符

    原文地址:C++ Coroutines: Understanding operator co_await 原文作者:lewissbaker 译文出自:掘金翻译计划 本文永久链接:github.com/ ...

  3. C++20协程初探!

    导语 | 本文推选自腾讯云开发者社区-[技思广益 · 腾讯技术人原创集]专栏.该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口.栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启 ...

  4. 通俗易懂的Go协程的引入及GMP模型简介

    本文根据Golang深入理解GPM模型加之自己的理解整理而来 Go协程的引入及GMP模型 一.协程的由来 1. 单进程操作系统 2. 多线程/多进程操作系统 3. 引入协程 二.golang对协程的处 ...

  5. 微信终端自研 C++协程框架的设计与实现

    作者:peterfan,腾讯 WXG 客户端开发工程师 背景 基于跨平台考虑,微信终端很多基础组件使用 C++ 编写,随着业务越来越复杂,传统异步编程模型已经无法满足业务需要.Modern C++ 虽 ...

  6. Python线程、进程、进程池、协程

    Python线程,切记Python的GIL特性 import threadingdef func():print(threading.current_thread().getName())passcl ...

  7. python协程详解

    目录 python协程详解 一.什么是协程 二.了解协程的过程 1.yield工作原理 2.预激协程的装饰器 3.终止协程和异常处理 4.让协程返回值 5.yield from的使用 6.yield ...

  8. Python_oldboy_自动化运维之路_线程,进程,协程(十一)

    本节内容: 线程 进程 协程 IO多路复用 自定义异步非阻塞的框架 线程和进程的介绍: 举个例子,拿甄嬛传举列线程和进程的关系: 总结: 1.工作最小单元是线程,进程说白了就是提供资源的 2.一个应用 ...

  9. python协程实现多任务

    目录 一.协程 二.协程和线程.进程的差异 三.使用yield实现协程 四.使用greenlet来实现协程 五.gevent来实现协程 一.协程 协程:又称微线程,纤程.是python中另外一种实现多 ...

最新文章

  1. short s1 = 1; s1 = s1 + 1;和short s1 = 1; s1 += 1;的区别
  2. Gnu/Linux 链接XServer方法
  3. 用动态数组模拟双向循环链表
  4. SAP ITS mobile 简介
  5. 字符串之括号的有效性
  6. HDU2602 (0-1背包)
  7. python两人一碰_python运用pygame库实现双人弹球小游戏
  8. java 文件名空格,java关于文件名带有空格的个人见解
  9. 小新pro13 重装注意_新款小新pro 13注意什么?买前必读
  10. 七. jenkins部署springboot项目(4)-linux环境--远程调试
  11. 【米课】思维导图与深度思考
  12. UE4之脚本导入fbx
  13. 使用DotNetOpenAuth搭建OAuth2.0授权框架——Demo代码简单说明
  14. 实现ls -l功能 和目录实现
  15. 基于Docker布署伪分布式hadoop环境(一)
  16. Windows系列服务器上配置JSP运行环境,以及网站上线
  17. Flash遮罩之溜光字制作二
  18. 【UI界面开发】背包系统一般思路
  19. Package zip is not available, but is referred to by another package.
  20. [springboot 开发单体web shop] 1. 前言介绍和环境搭建

热门文章

  1. Base64 SHA1 MD5
  2. Python获取用电情况数据-AHPU校园网
  3. 深度优先与广度优先的思想
  4. var和let和const_用故事讲解JavaScript的var,let和const变量
  5. 分分钟上手C#的委托和事件
  6. Git 的常规分支使用【dev和master】
  7. 【289期】Java 8 新特性:Comparator.naturalOrder | 自然排序
  8. 我的爸爸正在计算机前写报告,关于计算器的作文结尾
  9. 如何向外行解释,Bug是如何产生的?
  10. 读研不要钱?就业可定向推荐?国防科大计算机学院yyds