• C++ Coroutines: Understanding operator co_await
  • Originally written by lewissbaker
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: 7 Ethan
  • Proofreader: Razertory, Noahziheng

C++ coroutines: understandco_awaitThe operator

In my previous blog on coroutine theory, I covered some of the higher-level differences between functions and coroutines, but did not go into detail about the syntax and semantics described in the C++ coroutine technical specification (N4680).

The key new feature added to the coroutine specification in C++ is the ability to suspend coroutines and resume them later. The technical specification provides a mechanism to do this through the new CO_await operator.

Understanding how the co_await operator works can help us demystify the behavior of coroutines and understand how they are suspended and suspended. In this article, I will explain the mechanism of the co_await operator and introduce concepts related to the Awaitable and Awaiter types.

Before I dive into co_await, I want to briefly cover the technical specification of coroutines to provide some background.

What does the coroutine specification give us?

  • Three new keywords:co_await.co_yieldco_return
  • std::experimentalSeveral new types of namespaces:
    • coroutine_handle<P>
    • coroutine_traits<Ts... >
    • suspend_always
    • suspend_never
  • A common mechanism for library authors to interact with coroutines and customize their behavior.
  • A language tool that makes asynchronous code simpler!

The C++ coroutine technical specification provides tools in the language, which can be understood as the low-level assembly language of coroutines. These tools are difficult to use directly in a secure way and are primarily used by library authors to build higher-level abstractions that application developers can safely use.

These new low-level tools will be delivered to an upcoming language standard (probably C++20), along with some of the high-level types that accompany the standard library, which encapsulate these low-level building blocks and allow application developers to easily access the coroutines in a secure way.

Compiler – library interaction

Interestingly, the coroutine technical specification does not actually define the semantics of coroutines. It does not define how to generate the value returned to the caller, how to handle the return value passed to the CO_return statement, how to handle the exception passed out of the coroutine, and it does not define the thread that should restore the coroutine.

Instead, it specifies a general mechanism for library code to customize the behavior of coroutines by implementing types that match a particular interface. The compiler then generates code to invoke methods on the type instances provided by the library. This approach is similar to library authors customizing the implementation of scope-based for loops by defining begin()/end() methods or iterator types.

The coroutine technical specification does not specify any specific semantics for the mechanism of coroutines, which makes it a powerful tool. It allows library authors to define many different kinds of coroutines for different purposes.

For example, you can define a coroutine that asynchronously generates a single value, or one that delays the generation of a series of values, or one that simplifies the control flow to consume optional

values by exiting early if nullopt values are encountered.

The coroutine specification defines two types of interfaces: the Promise interface and the Awaitable interface.

The Promise interface specifies methods to customize the behavior of the coroutine itself. Library authors can customize events that occur when a coroutine is called, such as when it returns (either in the normal way or via an unhandled exception), or the behavior of any CO_await or CO_yield expression in a coroutine.

The Awaitable interface specifies methods to control the semantics of the CO_await expression. When a value is co_await, the code is transformed into a series of calls to methods on awaitable objects. It can specify whether to suspend the current coroutine, suspend the scheduled coroutine to execute some logic later when the coroutine resumes, and execute some logic after the coroutine resumes to produce the result of the CO_await expression.

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

Awaiters and Awaitables: Explain operatorsco_await

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

The co_await operator can only be used in the context of a coroutine. This is a bit semantically repetitive because, by definition, any function body that contains the co_await operator will be compiled as a coroutine.

Types that support the co_await operator are called Awaitable types.

Note that whether the co_await operator can be used as a type depends on the context in which the co_await expression appears. The Promise type used for the coroutine can change the meaning of the CO_await expression in the coroutine through its await_transform method (more on that later).

To be more specific where necessary, I like to use the term Normally Awaitable to describe types that support co_await operators in coroutine contexts where there is no await_transform member in the coroutine type. I like to use the term Contextually Awaitable to describe a type that supports only the co_await operator in the context of some types of coroutines because of the await_transform method in the promise type of coroutines. (I’m open to better suggestions for these names…)

The Awaiter type is a type that implements three special methods called part of the CO_await expression: await_ready, await_suspend, and await_resume.

Note that I “borrowed” the term “Awaiter” in the C# async keyword mechanism, which is implemented based on the GetAwaiter() method, which returns an object with an interface that is strikingly similar to the c++ Awaiter concept. For further details on C# awaiters, please see this blog post.

Note that the types can be both Awaitable and Awaiter.

When the compiler encounters a co_await

expression, it can actually convert it to many possible things, depending on the type involved.

Get Awaiter

The first thing the compiler does is generate code to get the Awaiter object for the wait value. In N4680 section 5.3.8(3), there are a number of steps to obtain aWAITER.

Let’s assume that the Promise object waiting for the coroutine has type P, and that the promise is an L-value reference to the promise object of the current coroutine.

If the Promise type P has a member named await_transform,

is first passed to promise.await_transform (

) to obtain the value of Awaitable. Otherwise, if the Promise type does not have an AWAIT_transform member, we use the result of the direct evaluation of

as an Awaitable object.


Then, if the Awaitable object has an overloaded operator co_await() available, it is called to get the Awaiter object. Otherwise, the awaitable object is used as an Awaiter object.

If we coded these rules into the get_awaitable() and get_awaiter() functions, they might look like this:

template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
  if constexpr (has_any_await_transform_member_v<P>)
    return promise.await_transform(static_cast<T&&>(expr));
  else
    return static_cast<T&&>(expr);
}

template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
  if constexpr (has_member_operator_co_await_v<Awaitable>)
    return static_cast<Awaitable&&>(awaitable).operator co_await();
  else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
    return operator co_await(static_cast<Awaitable&&>(awaitable));
  else
    return static_cast<Awaitable&&>(awaitable);
}
Copy the code

Waiting for Awaiter

Therefore, assuming that we have encapsulated the logic to convert the

result to an Awaiter object in the above function, the semantics of co_await

can be transformed (roughly) as follows:

{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
  if(! awaiter.await_ready()) {using handle_t = std::experimental::coroutine_handle<P>;

    using await_suspend_result_t =
      decltype(awaiter.await_suspend(handle_t::from_promise(p)));

    <suspend-coroutine>

    if constexpr (std::is_void_v<await_suspend_result_t>)
    {
      awaiter.await_suspend(handle_t::from_promise(p));
      <return-to-caller-or-resumer>
    }
    else
    {
      static_assert(
         std::is_same_v<await_suspend_result_t.bool>,
         "await_suspend() must return 'void' or 'bool'.");

      if (awaiter.await_suspend(handle_t::from_promise(p)))
      {
        <return-to-caller-or-resumer>
      }
    }

    <resume-point>
  }

  return awaiter.await_resume();
}
Copy the code

When a call to await_suspend() returns, a version of await_suspend() returns void unconditionally transfers execution back to the caller/restorer of the coroutine, whereas a version of await_suspend() returns bool allows an awaiter object to conditionally return and immediately resume the coroutine, Without returning the caller/restorer.

The bool returned version of await_Suspen () is useful in cases where aWAITER might start an asynchronous operation, which can sometimes be done synchronously. In the case that it completes synchronously, the await_suspend() method can return false to indicate that the coroutine should be resumed immediately and execution continued.

At < suspect-coroutine >, the compiler generates code to hold the current state of the coroutine and prepare it for recovery. This includes the breakpoint location where < Resum-point > is stored, and any values currently stored in registers are overflowed into coroutine snapshot memory.

After the < suspect-coroutine > operation completes, the current coroutine is considered suspended. You can observe that the first breakpoint of a suspended coroutine is in a call to await_suspend(). Once the coroutine is paused, it can be resumed or destroyed.

When the operation is complete, the await_suspend() method is responsible for scheduling and restoring (or destroying) the coroutine at some future point. Note that returning false from await_suspend() counts as a scheduled coroutine for immediate recovery on the current thread.

The purpose of the await_ready() method is to allow you to avoid the cost of a < suspect-coroutine > operation if you know that the operation completes synchronously without suspending.

A transition back to the caller or resumer is performed at the < return-caller-or-resumer > breakpoint, popping up the local stack frame but keeping the coroutine frame active.

When (or if) the suspended coroutine finally resumes, execution will resume at the < Resum-point > breakpoint. That is, get the result of the operation immediately before calling the await_resume() method.

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 is thrown from the co_await expression.

Note that if the exception is thrown from await_Suspen (), the coroutine will automatically resume and the exception will be thrown from the co_await expression without calling await_resume().

Handle coroutines

You may have noticed the use of the coroutine_handle

type, which is passed to the await_suspend() call of the co_await expression.

This type represents a nonowned handle to a coroutine frame that can be used to resume execution of the coroutine or to destroy the coroutine frame. It can also be used to access the promise objects of coroutines.

The coroutine_HANDLE type has the following interfaces:

namespace std::experimental
{
  template<typename Promise>
  struct coroutine_handle;

  template<>
  struct coroutine_handle<void>
  {
    bool done(a) const;

    void resume(a);
    void destroy(a);

    void* address(a) const;
    static coroutine_handle from_address(void* address);
  };

  template<typename Promise>
  struct coroutine_handle : coroutine_handle<void>
  {
    Promise& promise(a) const;
    static coroutine_handle from_promise(Promise& promise);

    static coroutine_handle from_address(void* address);
  };
}
Copy the code

The main method you will use on coroutine_handle when implementing the Awaitable type is.resume(), which should be called when the operation is complete and you want to resume execution of the waiting coroutine. Calling.resume() on coroutine_handle will reawaken a suspended coroutine at < Resum-point >. When the coroutine next encounters a < return-caller-or-resumer >, the call to.resume() will return.

The.destroy() method destroys the coroutine frame, calling the destructor of any variable in scope and freeing the memory used by the coroutine frame. In general, you don’t need (and should, in fact, avoid) calling.destroy() unless you’re a library writer that implements coroutine Promise types. Typically, the coroutine frame will be owned by some RAII type returned from a call to the coroutine. So calling.destroy() without working with RAII objects can result in a double destroy error.

The.promise() method returns a reference to the coroutine’s promise object. However, like.destroy(), it is usually only useful if you create coroutine promise types. You should think of the promise object of a coroutine as an internal implementation detail. For most regular Awaitable types, you should use coroutine_handle

as the parameter type for the await_suspend() method instead of coroutine_handle .

The coroutine_handle

:: from_PROMISE (P&promise) function allows the coroutine handle to be reconstructed from a reference to the coroutine’s promise object. Note that you must ensure that type P matches exactly the specific PROMISE type used for coroutine frames; When the concrete promise type is Derived, attempting to construct coroutine_handle causes an error with undefined behavior.

The.address()/from_address() function allows conversion of coroutine handles to void* Pointers. This is mainly to allow for passing as a “context” parameter to existing C-style apis, so you may find it useful to implement Awaitable types in some cases. However, in most cases, I find it necessary to pass additional information to the callback in this ‘context’ parameter, So I usually end up storing coroutine_handle in the structure and passing a pointer to the structure in the ‘context’ argument rather than returning the value with.address().

Asynchronous code with no synchronization

A powerful design feature of the co_await operator is the ability to execute code after the coroutine has hung but before the execution is returned to the caller/restorer.

This allows the Awaiter object to initiate an asynchronous operation after the coroutine has been suspended, passing the coroutine_handle of the suspended coroutine to the operator, and when the operation is complete (possibly on another thread) it can safely resume the operation without any additional synchronization.

For example, starting an asynchronous read operation inside await_suspend() when the coroutine is already suspended means that we can resume the coroutine when the operation completes, without requiring any thread synchronization to coordinate the thread that started the operation and the thread that completed the operation.

Time     Thread 1                           Thread 2
  |      --------                           --------
  |      ....                               Call OS - Wait for I/O event
  |      Call await_ready()                    |
  |      <supend-point>                        |
  |      Call await_suspend(handle)            |
  |        Store handle in operation           |
  V        Start AsyncFileRead ---+            V
                                  +----->   <AsyncFileRead Completion Event>
                                            Load coroutine_handle from operation
                                            Call handle.resume()
                                              <resume-point>
                                              Call to await_resume()
                                              execution continues....
           Call to AsyncFileRead returns
         Call to await_suspend() returns
         <return-to-caller/resumer>
Copy the code

One thing to note in particular when using this method is that if you start publishing operations on coroutine handles to other threads, another thread can resume the coroutine on another thread before await_suspend() returns, continuing to execute concurrently with the rest of the await_suspend() method.

The first thing to do when a coroutine recovers is to call await_resume() to get the result, and often immediately destroy the Awaiter object (the this pointer from the await_suspend() call). Before await_suspend() returns, the coroutine may run complete, destroying both the coroutine and the Promise object.

So in the await_suspend() method, if you can restore the coroutine on another thread at the same time, you need to be sure to avoid accessing either the this pointer or the.promise() object of the coroutine, since both may have been destroyed. In general, the only local variable in await_suspend() that can be safely accessed after an operation is started and the coroutine is scheduled to resume.

Comparison with Stackful coroutines

I’d like to expand a bit by comparing stackless coroutines from the coroutine technical specification with their ability to execute logic once the coroutine has been suspended with some of the existing common coroutine tools such as Win32 fibers or Boost :: Context.

For many Stackful coroutine frameworks, the pause operation of one coroutine is combined with the recovery operation of another to form a “context-switch” operation. With this “context-switch” operation, there is usually no chance to execute logic after suspending the current coroutine, but before moving execution to another coroutine.

This means that if we want to implement a similar asynchronous file read operation on top of a Stackful coroutine, we must start the operation before suspending the coroutine. Thus, an operation can be completed on another thread before the coroutine is paused and is eligible for recovery. This potential competition between operations done on another thread and coroutine suspension requires some kind of thread synchronization to arbitrate and determine the winner.

You can solve this problem by using the Trampoline Context, which can initiate operations on behalf of the startup context after the initialization context has been suspended. However, this will require additional infrastructure and additional context switches to make it work, and the overhead this introduces may be greater than the cost of it trying to avoid synchronization.

Avoiding memory allocation

Asynchronous operations typically need to store some state for each operation to track the progress of the operation. This state usually needs to persist for the duration of the operation and is released only after the operation is complete.

For example, calling the asynchronous Win32 I/O function requires you to allocate and pass Pointers to the OVERLAPPED structure. It is the responsibility of the caller to ensure that this pointer remains valid until the operation completes.

With traditional callback-based apis, this state typically needs to be allocated on the heap to ensure it has an appropriate life cycle. If you perform many operations, you may need to assign and release this state for each operation. If performance becomes an issue, you can use a custom allocator to allocate these state objects from the memory pool.

At the same time, we can avoid allocating memory on the heap for operation state when using coroutines by taking advantage of the fact that local variables in the coroutine frame remain active after the coroutine hangs.

By placing each operation state in an Awaiter object, we can effectively “borrow” the memory from the coroutine frame to store each operation state for the duration of the CO_await expression. Once the operation is complete, the coroutine resumes and destroys the Awaiter object, freeing up memory in the coroutine frame for use by other local variables.

Finally, coroutine frames can still be allocated on the heap. However, once allocated, coroutine frames can use this heap allocation to perform many asynchronous operations.

If you think about it, coroutine frames act like a high-performance Arena memory allocator. The compiler calculates the total arena size required for all local variables at compile time, and is then able to allocate memory to local variables as needed, with zero overhead! Try beating it with custom allocator;)

Example: Implementing a simple thread synchronization primitive

Now that we have covered many of the mechanisms of the co_await operator, I want to show how to put this knowledge into practice by implementing a basic wait-ready synchronization primitive: asynchronous manual reset events.

The basic requirement for this event is that it needs to execute multiple coroutines simultaneously to become Awaitable state, and while waiting, the waiting coroutines need to be suspended until a thread calls the.set() method, at which point any waiting coroutines will resume. If a thread has already called.set(), the coroutine should continue, not suspend.

Ideally, we would also like it to be noexcept, without allocation on the heap or lockless implementation.

Update: Addedasync_manual_reset_eventThe sample

Example usage is as follows:

T value;
async_manual_reset_event event;

// A single call to produce a value
void producer(a)
{
  value = some_long_running_computation();

  // Publish the value by setting the event.
  event.set(a); }// Supports multiple concurrent consumers
task<> consumer()
{
  // Wait until the event is signalled by call to event.set()
  // in the producer() function.
  co_await event;

  // Now it's safe to consume 'value'
  // This is guaranteed to 'happen after' assignment to 'value'
  std: :cout << value << std: :endl;
}
Copy the code

Let’s first consider the possible states for this event: not set and set.

When it is in the ‘not set’ state, a queue of (possibly empty) coroutines is waiting for it to become the ‘set’ state.

When it is in the ‘set’ state, there are no waiting coroutines because events in co_WAIT state can continue without pausing.

This state can actually be represented by a STD :: atomic

.

  • Reserve a special pointer value for the ‘set’ state. In this case, we will use the eventthisPointer because we know we can’t have the same address as any list item.
  • Otherwise, the event is in the ‘not set’ state, and the value is a pointer to the head of the singly linked list waiting for the coroutine structure.

We can avoid additional calls to allocate nodes to the linked list on the heap by storing them in an ‘awaiter’ object placed within the coroutine frame.

Let’s start with a class interface that looks like this:

class async_manual_reset_event
{
public:

  async_manual_reset_event(bool initiallySet = false) noexcept;

  // No copying/moving
  async_manual_reset_event(const async_manual_reset_event&) = delete;
  async_manual_reset_event(async_manual_reset_event&&) = delete;
  async_manual_reset_event& operator= (const async_manual_reset_event&) = delete;
  async_manual_reset_event& operator=(async_manual_reset_event&&) = delete;

  bool is_set(a) const noexcept;

  struct awaiter;
  awaiter operator co_await(a) const noexcept;

  void set(a) noexcept;
  void reset(a) noexcept;

private:

  friend struct awaiter;

  // - 'this' => set state
  // - otherwise => not set, head of linked list of awaiter*.
  mutable std::atomic<void*> m_state;

};
Copy the code

We have a fairly straightforward and simple interface. The thing to notice at this point is that it has an operator co_await() method, which returns an awaiter type that has not yet been defined.

Now let’s define the awaiter type

Define the Awaiter type

First, it needs to know which async_manual_reset_event object it will wait for, so it needs an application of this event and the corresponding constructor to initialize it.

It also needs to act as a node in the linked list of AWaiter values, so it needs to hold a pointer to the next AWaiter object in the list.

It also needs to store the coroutine_handle of the waiting coroutine that is executing the CO_await expression so that the event can resume the coroutine when the event changes to the ‘set’ state. We don’t care what the promise type of coroutines is, so we’ll just use coroutine_handle <> (which is short for coroutine_handle

).

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

When we put it all together, the basic aWaiter class interface looks like this:

struct async_manual_reset_event::awaiter
{
  awaiter(const async_manual_reset_event& event) noexcept
  : m_event(event)
  {}

  bool await_ready(a) const noexcept;
  bool await_suspend(std::experimental::coroutine_handle<> awaitingCoroutine) noexcept;
  void await_resume(a) noexcept {}

private:

  const async_manual_reset_event& m_event;
  std::experimental::coroutine_handle<> m_awaitingCoroutine;
  awaiter* m_next;
};
Copy the code

Now, when we execute co_await an event, we do not want to wait for the coroutine to pause if the event is already set. Therefore, if the event is set, we can define await_ready() to return true.

bool async_manual_reset_event::awaiter::await_ready() const noexcept
{
  return m_event.is_set();
}
Copy the code

Next, let’s look at the await_suspend() method. This is usually a place where unexplainable things happen to Awaitable types.

First, it needs to store a handle to the waiting coroutine to the m_awaitingCoroutine member so that the event can call.resume() on it later.

Then, when we’re done with this, we need to try to automatically add the AWaiter to our waiters’ linked list. If we successfully join it, then we return true to indicate that we do not want to restore the coroutine immediately, otherwise, if we find that the event has concurrently changed to the set state, then we return false to indicate that the coroutine should resume immediately.

bool async_manual_reset_event::awaiter::await_suspend(
  std::experimental::coroutine_handle<> awaitingCoroutine) noexcept
{
  // Special m_state value that indicates the event is in the 'set' state.
  const void* const setState = &m_event;

  // Remember the handle of the awaiting coroutine.
  m_awaitingCoroutine = awaitingCoroutine;

  // Try to atomically push this awaiter onto the front of the list.
  void* oldValue = m_event.m_state.load(std::memory_order_acquire);
  do
  {
    // Resume immediately if already in 'set' state.
    if (oldValue == setState) return false; 

    // Update linked list to point at current head.
    m_next = static_cast<awaiter*>(oldValue);

    // Finally, try to swap the old list head, inserting this awaiter
    // as the new list head.
  } while(! m_event.m_state.compare_exchange_weak( oldValue,this.std::memory_order_release,
             std::memory_order_acquire));

  // Successfully enqueued. Remain suspended.
  return true;
}
Copy the code

Note that when loading the old state, we use ‘acquire’ to see the memory order, and if we read the special ‘set’ value, then we can see the writes that took place before calling ‘set()’.

If compact-exchange executes successfully, we need the state of ‘release’ so that subsequent calls to ‘set()’ will see our write to m_awaitingConoutine and the previous write to the coroutine state.

Complete the rest of the event class

Now that we have defined the awaiter type, let’s go back to the implementation of the async_manual_reset_event method.

The first is the constructor. It needs to be initialized to a ‘not set’ state with an empty waiters chain table (i.e., NULlPTR) or to a ‘set’ state (i.e., this).

async_manual_reset_event::async_manual_reset_event(
  bool initiallySet) noexcept
: m_state(initiallySet ? this : nullptr)
{}
Copy the code

Next, the is_set() method is very simple – ‘set’ if it has the special value this:

bool async_manual_reset_event::is_set() const noexcept
{
  return m_state.load(std::memory_order_acquire) == this;
}
Copy the code

Then there is the reset() method, if it is in the ‘set’ state, we want it to switch to the ‘not set’ state, otherwise it stays the same.

void async_manual_reset_event::reset() noexcept
{
  void* oldValue = this;
  m_state.compare_exchange_strong(oldValue, nullptr.std::memory_order_acquire);
}
Copy the code

With the set() method, we want to convert the current state to the ‘set’ state by using a special ‘set’ value (this), and then check what the original value was. If there are any waiting coroutines, we want to restore them sequentially before returning.

void async_manual_reset_event::set(a)noexcept
{
  // Needs to be 'release' so that subsequent 'co_await' has
  // visibility of our prior writes.
  // Needs to be 'acquire' so that we have visibility of prior
  // writes by awaiting coroutines.
  void* oldValue = m_state.exchange(this.std::memory_order_acq_rel);
  if(oldValue ! =this)
  {
    // Wasn't already in 'set' state.
    // Treat old value as head of a linked-list of waiters
    // which we have now acquired and need to resume.
    auto* waiters = static_cast<awaiter*>(oldValue);
    while(waiters ! =nullptr)
    {
      // Read m_next before resuming the coroutine as resuming
      // the coroutine will likely destroy the awaiter object.
      auto* next = waiters->m_next; waiters->m_awaitingCoroutine.resume(); waiters = next; }}}Copy the code

Finally, we need to implement the operator co_await() method. This simply requires constructing an aWaiter object.

async_manual_reset_event::awaiter
async_manual_reset_event::operator co_await(a) const noexcept
{
  return awaiter{ *this };
}
Copy the code

We are finally done with it, a waiting asynchronous manual reset event with no lock, no memory allocation, noexcept implementation.

If you want to try out the code, or see it compiled under MSVC and Clang, check it out on Godbolt.

You can also find implementations of this class in the CPPcoro library, as well as many other useful Awaitable types, such as async_mutex and asynC_auto_reset_event.

conclusion

This article describes how to implement and define the operator co_await according to the concepts Awaitable and Awaiter.

It also shows how to implement a waiting asynchronous thread synchronization primitive that takes advantage of the fact that awaiter objects are allocated on coroutine frames to avoid additional heap allocation.

I hope this article has helped you get a better understanding of the new co_await operator.

In the next blog post, I’ll explore the Promise concept and how coroutine type authors can customize the behavior of their coroutines.

Thank you

I particularly want to thank Gor Nishanov for patiently and enthusiastically answering many of my questions about coroutines over the past few years.

In addition, Eric Niebler reviewed and provided feedback on an early draft of this article.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.