我是靠谱客的博主 舒服蜜粉,最近开发中收集的这篇文章主要介绍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 其中exprvoid类型
    • 从返回空值的协程的末尾脱落。在这种情况下,如果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 newnothrow形式,并且在分配失败时,协程立即将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 /*cv-qual*/&, 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 /*cv-qual*/&&, 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的协程返回类型确定。正式地,设RArgs…分别表示协程的返回类型和参数类型列表,classsT`和/cv-qual/(如果有的话)分别表示协程所属的类类型和它的cv限定条件。如果它被定义为一个非静态成员函数,它的Promise类型由:

  • std::coroutine_traits<R, Args...>::promise_type,如果协程未定义为非静态成员函数,
  • std::coroutine_traits<R, ClassT /*cv-qual*/&, Args...>::promise_type, 如果协程定义为非rvalue-reference限定的非静态成员函数,
  • std::coroutine_traits<R, ClassT /*cv-qual*/&&, 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
这一部分尚未完成

最后

以上就是舒服蜜粉为你收集整理的C++20-协程(coroutine)的全部内容,希望文章能够帮你解决C++20-协程(coroutine)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部