天天看点

C++20-协程(coroutine)

原文: https://en.cppreference.com/w/cpp/language/coroutines

Coroutines

A coroutine is a function that can suspend execution to be resumed later. Coroutines are stackless: they suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack. This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks), and also supports algorithms on lazy-computed infinite sequences and other uses.

A function is a coroutine if its definition does any of the following:

coroutine是一个可以被挂起和恢复的函数. 协程是无堆栈的:它们通过返回到调用者来暂停执行,恢复执行所需的数据与堆栈分开存储。这允许异步执行的顺序代码(例如,在没有显式回调的情况下处理非阻塞I/O),也支持延迟计算无限序列的算法和其他用途。

如果一个函数的定义有以下任何一种情况,那么它就是协程:

  • 使用

    co_await

    操作符暂停执行,直到恢复
    task<> tcp_echo_server() {
      char data[1024];
      for (;;) {
        size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
      }
    }
               
  • 使用关键字

    co_yield

    暂停执行,返回一个值
    generator<int> iota(int n = 0) {
      while(true)
        co_yield n++;
    }
               
  • 使用关键字

    co_return

    完成执行,返回一个值
    lazy<int> f() {
    	  co_return 7;
    	}
               

每个协程都必须有一个返回类型来满足以下的许多要求。

Restrictions

Coroutines cannot use variadic arguments, plain return statements, or placeholder return types (auto or Concept).

Constexpr functions, constructors, destructors, and the main function cannot be coroutines.

限制条件:

协程不能使用可变参数( variadic arguments)、普通返回(

return

)语句或

占位符返回类型

(auto或Concept)。Constexpr函数、

构造函数

析构函数

main函数

不能是协程。

执行

Execution

Each coroutine is associated with

  • the

    promise

    object, manipulated from inside the coroutine. The coroutine submits its result or exception through this object.
  • the

    coroutine handle

    , manipulated from outside the coroutine. This is a non-owning handle used to resume execution of the coroutine or to destroy the coroutine frame.
  • the

    coroutine state

    , which is an internal, heap-allocated (unless the allocation is optimized out), object that contains
    • the promise object
    • the parameters (all copied by value)
    • some representation of the current suspension point, so that resume knows where to continue and destroy knows what local variables were in scope
    • local variables and temporaries whose lifetime spans the current suspension point

每个coroutine的关联对象:

  • promise

    对象,从协程内部操纵。协程通过此对象提交其结果或异常。
  • corotine handle

    (协程句柄),从协程外部操纵。这是一个非所有者(non-owning)句柄,用于恢复协程的执行或销毁协程帧。
  • coroutine state

    (协程状态),它是一个内部的堆分配对象(除非分配被优化),包含:
    • promise

      对象
    • 参数(都是通过值拷贝)
    • 当前挂起点的一些标记信息(representation),这样resume就知道在哪里继续,destroy就知道哪些局部变量在作用域中
    • 生存期跨越当前挂起点的局部变量和临时变量
When a coroutine begins execution, it performs the following:
  • allocates the coroutine state object using

    operator new

    (see below)
  • copies all function parameters to the coroutine state: by-value parameters are moved or copied, by-reference parameters remain references (and so may become dangling if the coroutine is resumed after the lifetime of referred object ends)
  • calls the constructor for the promise object. If the promise type has a constructor that takes all coroutine parameters, that constructor is called, with post-copy coroutine arguments. Otherwise the default constructor is called.
  • calls

    promise.get_return_object()

    and keeps the result in a local variable. The result of that call will be returned to the caller when the coroutine first suspends. Any exceptions thrown up to and including this step propagate back to the caller, not placed in the promise.
  • calls

    promise.initial_suspend()

    and

    co_awaits

    its result. Typical Promise types either return a suspend_always, for lazily-started coroutines, or suspend_never, for eagerly-started coroutines.
  • when

    co_await promise.initial_suspend()

    resumes, starts executing the body of the coroutine

当协程开始执行时,它会执行以下操作:

  • 使用

    operator new

    分配协程状态对象(见下文)
  • 将所有函数形参复制到协程状态:如果是按值传参则其被移动(move)或复制,如果是引用传参则保留引用(因此,如果在被引用对象的生命周期结束后恢复协程,可能会变得悬空, 因此, 程序员注意对象的生命周期)
  • 调用promise对象的构造函数。如果promise类型有一个接受所有协程参数的构造函数,则调用该构造函数,并带有复制后的协程参数。否则,将调用默认构造函数。
  • 调用

    promise.get_return_object()

    并将结果保存在一个局部变量中。当协程第一次挂起时,该调用的结果将返回给调用者。到此步骤为止抛出的任何异常(包括此步骤)都会传播回调用者,而不是放在promise中。
  • 调用

    promise.initial_suspend()

    co_await

    其结果。典型的Promise类型要么为lazily-started(慢启动)协程返回一个

    suspend_always

    ,要么为eagerly-started(急启动)协程返回一个

    suspend_never

  • co_await promise.initial_suspend()

    恢复时,开始执行协程体
When a coroutine reaches a suspension point
  • the return object obtained earlier is returned to the caller/resumer, after implicit conversion to the return type of the coroutine, if necessary.
When a coroutine reaches the

co_return

statement, it performs the following:
  • calls promise.return_void() for
    • co_return

      ;
    • co_return expr

      where expr has type void
    • falling off the end of a void-returning coroutine. The behavior is undefined if the Promise type has no

      Promise::return_void()

      member function in this case.
  • or calls

    promise.return_value(expr)

    for

    co_return

    expr where expr has non-void type
  • destroys all variables with automatic storage duration in reverse order they were created.
  • calls

    promise.final_suspend()

    and

    co_awaits

    the result.
If the coroutine ends with an uncaught exception, it performs the following:
  • catches the exception and calls

    promise.unhandled_exception()

    from within the catch-block
  • calls

    promise.final_suspend()

    and

    co_awaits

    the result (e.g. to resume a continuation or publish a result). It’s undefined behavior to resume a coroutine from this point.
When the coroutine state is destroyed either because it terminated via

co_return

or uncaught exception, or because it was destroyed via its handle, it does the following:
  • calls the destructor of the promise object.
  • calls the destructors of the function parameter copies.
  • calls

    operator delete

    to free the memory used by the coroutine state
  • transfers execution back to the caller/resumer.

当协程到达一个暂停点时

  • 如果需要,在隐式转换为协程的返回类型之后,前面获得的返回对象返回给caller/resumer。

当协程到达

co_return

语句时,它执行以下操作:

  • 调用

    promise.return_void()

    • co_return

      ;
    • co_return expr

      其中

      expr

      void

      类型
    • 从返回空值的协程的末尾脱落。在这种情况下,如果Promise类型没有

      Promise::return_void()

      成员函数,则该行为是未定义(undefined的。
  • 或者调用

    promise.return_value(expr)

    来获取

    co_return expr

    ,其中expr为非void类型
  • 按创建时的相反顺序销毁所有自动变量。
  • 调用

    promise.final_suspend()

    co_await

    结果。

如果协程以未捕获的异常结束,它将执行以下操作:

  • 捕获异常并在catch块中调用

    promise.unhandled_exception()

  • 调用

    promise.final_suspend()

    co_await

    结果(例如恢复延续或发布结果)。从这一点恢复协程是未定义的行为。

当协程状态被销毁是因为它通过

co_return

或未捕获的异常终止,或因为它是通过它的句柄销毁的,它会执行以下操作:

  • 调用

    promise

    对象的析构函数。
  • 调用函数参数副本的析构函数。
  • 调用

    operator delete

    来释放协程状态所使用的内存
  • 将执行传输回caller/resumer。

堆分配

Heap allocation

coroutine state is allocated on the heap via non-array operator new.

If the Promise type defines a class-level replacement, it will be used, otherwise global operator new will be used.

If the Promise type defines a placement form of operator new that takes additional parameters, and they match an argument list where the first argument is the size requested (of type std::size_t) and the rest are the coroutine function arguments, those arguments will be passed to operator new (this makes it possible to use leading-allocator-convention for coroutines)

The call to operator new can be optimized out (even if custom allocator is used) if

  • The lifetime of the coroutine state is strictly nested within the lifetime of the caller, and
  • the size of coroutine frame is known at the call site

in that case, coroutine state is embedded in the caller’s stack frame (if the caller is an ordinary function) or coroutine state (if the caller is a coroutine)

If allocation fails, the coroutine throws std::bad_alloc, unless the Promise type defines the member function Promise::get_return_object_on_allocation_failure(). If that member function is defined, allocation uses the nothrow form of operator new and on allocation failure, the coroutine immediately returns the object obtained from Promise::get_return_object_on_allocation_failure() to the caller.

协程状态是通过非数组操作符new在堆上分配的。

如果Promise类型定义了类级别的

operator new

,则使用它,否则将使用全局

operator new

如果Promise类型定义了一个需要额外的参数的

operator new

作为替代,和他们匹配一个参数列表,第一个参数是请求的大小(类型的std:: size_t),其余是协同程序函数参数,这些参数将传递给

operator new

的(这使它可以使用leading-allocator-convention协程)

对operator new的调用可以优化出来(即使使用了自定义分配器),如果:

  • 协程状态的生存期严格嵌套在调用者的生存期内,并且
  • 协程帧的大小在调用站点是已知的

    在这种情况下,协程状态被嵌入到调用者的堆栈框架中(如果调用者是一个普通函数)或协程状态(如果调用者是一个协程)

如果分配失败,则该coroutine将抛出

std::bad_alloc

,除非Promise类型定义了成员函数

Promise::get_return_object_on_allocation_failure()

。如果定义了该成员函数,则allocation使用

operator new

nothrow

形式,并且在分配失败时,协程立即将

Promise::get_return_object_on_allocation_failure()

获得的对象返回给调用者。

Promise

The Promise type is determined by the compiler from the return type of the coroutine using

std::coroutine_traits

.

Formally, let R and Args… denote the return type and parameter type list of a coroutine respectively, ClassT and /cv-qual/ (if any) denote the class type to which the coroutine belongs and its cv-qualification respectively if it is defined as a non-static member function, its Promise type is determined by:

  • std::coroutine_traits<R, Args...>::promise_type

    , if the coroutine is not defined as a non-static member function,
  • std::coroutine_traits<R, ClassT &, Args...>::promise_type

    , if the coroutine is defined as a non-static member function that is not rvalue-reference-qualified,
  • std::coroutine_traits<R, ClassT &&, Args...>::promise_type

    , if the coroutine is defined as a non-static member function that is rvalue-reference-qualified.
For example:
  • If the coroutine is defined as

    task<float> foo(std::string x, bool flag);

    , then its Promise type is

    std::coroutine_traits<task<float>, std::string, bool>::promise_type

    .
  • If the coroutine is defined as

    task<void> my_class::method1(int x) const

    ;, its Promise type is

    std::coroutine_traits<task<void>, const my_class&, int>::promise_type

    .
  • If the coroutine is defined as

    task<void> my_class::method1(int x) &&;

    , its Promise type is

    std::coroutine_traits<task<void>, my_class&&, int>::promise_type

    .

Promise类型由编译器根据使用

std::coroutine_traits

的协程返回类型确定。正式地,设

R

Args…分别表示协程的返回类型和参数类型列表,

classsT`和/cv-qual/(如果有的话)分别表示协程所属的类类型和它的cv限定条件。如果它被定义为一个非静态成员函数,它的Promise类型由:

  • std::coroutine_traits<R, Args...>::promise_type

    ,如果协程未定义为非静态成员函数,
  • std::coroutine_traits<R, ClassT &, Args...>::promise_type

    , 如果协程定义为非rvalue-reference限定的非静态成员函数,
  • std::coroutine_traits<R, ClassT &&, Args...>::promise_type

    , 如果协程定义为rvalue-reference限定的非静态成员函数。

举例:

  • 如果coroutine被定义为

    task<float> foo(std::string x, bool flag);

    , 那么它的Promise类型是

    std::coroutine_traits<task<float>, std::string, bool>::promise_type

    .
  • 如果coroutine被定义为

    task<void> my_class::method1(int x) const

    ;, 那么它的Promise类型是

    std::coroutine_traits<task<void>, const my_class&, int>::promise_type

    .
  • 如果coroutine被定义为

    task<void> my_class::method1(int x) &&;

    , 那么它的Promise类型是

    std::coroutine_traits<task<void>, my_class&&, int>::promise_type

    .

co_await

The unary operator co_await suspends a coroutine and returns control to the caller. Its operand is an expression whose type must either define operator co_await, or be convertible to such type by means of the current coroutine’s

Promise::await_transform

一元操作符

co_await

挂起协程并将控制权返回给调用者。它的操作数是一个表达式,其类型必须定义操作符

co_await

,或者通过当前协程的

Promise::await_transform

可转换为该类型

co_await expr		
           
First, expr is converted to an awaitable as follows:
  • if expr is produced by an initial suspend point, a final suspend point, or a yield expression, the awaitable is expr, as-is.
  • otherwise, if the current coroutine’s Promise type has the member function await_transform, then the awaitable is promise.await_transform(expr)
  • otherwise, the awaitable is expr, as-is.

首先,expr被转换为可等待对象,如下所示:

  • 如果expr是由初始挂起点、最终挂起点或yield表达式生成的,则可等待对象按实际情况为expr。
  • 否则,如果当前协程的Promise类型有成员函数await_transform,那么可等待对象就是Promise .await_transform(expr)
  • 否则,可等待对象就是expr。
If the expression above is a prvalue, the awaiter object is a temporary materialized from it. Otherwise, if the expression above is an glvalue, the awaiter object is the object to which it refers.

如果上面的表达式是prvalue,则awaiter对象是它的临时实体化对象。否则,如果上面的表达式是glvalue,则awaiter对象就是它所引用的对象。

Then, awaiter.await_ready() is called (this is a short-cut to avoid the cost of suspension if it’s known that the result is ready or can be completed synchronously). If its result, contextually-converted to bool is false then

然后,调用

await .await_ready()

如果知道结果已经就绪或可以同步完成,这是一种避免挂起代价的捷径)。如果它的结果,上下文转换为bool则为false

The coroutine is suspended (its coroutine state is populated with local variables and current suspension point).

awaiter.await_suspend(handle) is called, where handle is the coroutine handle representing the current coroutine. Inside that function, the suspended coroutine state is observable via that handle, and it’s this function’s responsibility to schedule it to resume on some executor, or to be destroyed (returning false counts as scheduling)

协程被挂起(它的协程状态由局部变量和当前挂起点填充)。调用await .await_suspend(句柄),其中句柄是表示当前协程的协程句柄。在这个函数内部,挂起的协程状态是可以通过这个句柄观察到的,这个函数的责任是安排它在某些执行器上恢复,或被销毁(返回错误计数作为调度)。

  • if await_suspend returns void, control is immediately returned to the caller/resumer of the current coroutine (this coroutine remains suspended), otherwise
  • if await_suspend returns bool,
    • the value true returns control to the caller/resumer of the current coroutine
    • the value false resumes the current coroutine.
  • if await_suspend returns a coroutine handle for some other coroutine, that handle is resumed (by a call to handle.resume()) (note this may chain to eventually cause the current coroutine to resume)
  • if await_suspend throws an exception, the exception is caught, the coroutine is resumed, and the exception is immediately re-thrown
  • 如果await_suspend返回void,则控制权立即返回给当前协程的调用者/恢复者(该协程保持挂起状态),否则
    • 如果await_suspend返回bool值,
    • 值true将控制权返回给当前协程的调用者/恢复者
    • 如果值为false,则恢复当前协程。
  • 如果await_suspend返回其他协程的协程句柄,该句柄将被恢复(通过调用handle.resume())(注意这可能导致当前协程最终恢复)
  • 如果await_suspend抛出异常,异常被捕获,协程被恢复,异常立即被重新抛出
Finally, awaiter.await_resume() is called, and its result is the result of the whole

co_await expr

expression.

If the coroutine was suspended in the co_await expression, and is later resumed, the resume point is immediately before the call to awaiter.await_resume().

最后,调用

await .await_resume()

,其结果是整个

co_await expr

表达式的结果。

如果协程在

co_await

表达式中被挂起,然后被恢复,恢复点就在调用

await .await_resume()

之前。

Note that because the coroutine is fully suspended before entering awaiter.await_suspend(), that function is free to transfer the coroutine handle across threads, with no additional synchronization. For example, it can put it inside a callback, scheduled to run on a threadpool when async I/O operation completes. In that case, since the current coroutine may have been resumed and thus executed the awaiter object’s destructor, all concurrently as

await_suspend()

continues its execution on the current thread,

await_suspend()

should treat

*this

as destroyed and not access it after the handle was published to other threads.

注意,因为协程在进入

await .await_suspend()

之前已经完全挂起,所以该函数可以自由地跨线程传递协程句柄,而不需要额外的同步操作。例如,它可以将其放在回调函数中,计划在异步I/O操作完成时在线程池中运行。在这种情况下,因为当前的协同程序可能已经恢复,因此等待对象的析构函数执行,所有并发

await_suspend()

在当前线程继续执行,

await_suspend()

应该把

*this

当作已经销毁并且在将句柄发布到其他线程之后不要再去访问它。

例子

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out) {
  struct awaitable {
    std::jthread* p_out;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> h) {
      std::jthread& out = *p_out;
      if (out.joinable())
        throw std::runtime_error("Output jthread parameter not empty");
      out = std::jthread([h] { h.resume(); });
      // Potential undefined behavior: accessing potentially destroyed *this
      // std::cout << "New thread ID: " << p_out->get_id() << '\n';
      std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
    }
    void await_resume() {}
  };
  return awaitable{&out};
}
 
struct task{
  struct promise_type {
    task get_return_object() { return {}; }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
  };
};
 
task resuming_on_new_thread(std::jthread& out) {
  std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
  co_await switch_to_new_thread(out);
  // awaiter destroyed here
  std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main() {
  std::jthread out;
  resuming_on_new_thread(out);
}
           

需要使用

gcc 10.2

进行编译

$ g++ --version
g++ (Ubuntu 10.2.0-5ubuntu1~20.04) 10.2.0
$ g++ coroutine.cc -std=c++20 -fcoroutines
$ ./a.out 
Coroutine started on thread: 140421255046976
New thread ID: 140421255042816
Coroutine resumed on thread: 140421255042816
           

This section is incomplete

Reason: examples

这一部分尚未完成

co_yield

co_yield

Yield-expression returns a value to the caller and suspends the current coroutine: it is the common building block of resumable generator functions

co_yield expr

co_yield braced-init-list

It is equivalent to

co_await promise.yield_value(expr)

A typical generator’s yield_value would store (copy/move or just store the address of, since the argument’s lifetime crosses the suspension point inside the co_await) its argument into the generator object and return std::suspend_always, transferring control to the caller/resumer.

Yield-expression返回一个值给调用者,并挂起当前协程:它可以构建可恢复生成器函数(类似python中的

yield

)

co_yield expr		
co_yield braced-init-list	
           

它等价于

co_await promise.yield_value(expr)
           

一个典型的生成器的

yield_value

将其参数存储(复制/移动或仅仅存储其地址,因为参数的生命周期跨越了

co_await

内部的悬挂点)到生成器对象中,并返回

std:: susend_always

,将控制权转移给caller/resumer。

This section is incomplete

Reason: examples

这一部分尚未完成