【网络-性能】协程2-初识 C++20 协程

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

本文结合协程示例源码,和编译器转换后的代码,对 C++ 20 协程的构成,及其执行流程进行分析和整理。


1 C++20 协程概述

1.1 概念说明

cppreference 说明:

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.

协程的特性使得代码可以“以顺序的方式编写,以异步的方式执行”(例如,在处理非阻塞 I/O 操作时,并不需要显式的回调函数);此外,协程还支持针对无限序列的惰性计算算法,以及其他的用途。

结合 协程1-并发基础概念 => 4 协程 部分的介绍,也许可以帮助我们更好的理解上述说明,以及本文后续内容。

1.2 协程函数

协程函数需要满足的条件包括:

  1. 函数体至少包含下表某一关键字:
    序号关键字功能代码示例
    1co_awaitco_await a 表达式:用于挂起执行,直到被恢复。
    a 的类型必须 满足一组指定的要求
    co_await async_write(socket, buffer);
    2co_yieldco_yield v 表达式:用于挂起执行,并返回一个值。
    co_yieldco_await 的语法糖
    co_yield n++;
    3co_returnco_return 语句:用于结束执行,并可以返回一个值。co_return 7;
  2. 函数返回类型必须 满足一组指定的要求
  3. 函数的其它限制:
    • 不能使用变长实参、普通的 return 语句、占位符返回类型(autoConcept);
    • consteval 函数、constexpr 函数、构造函数、析构函数、main 函数不能作为协程。

根据以上信息可以判断,理解协程的两个重点分别为:协程返回类型co_await 表达式

协程交互示意:
协程交互示例

1.3 编译环境

可以从 cppreference 查看 Coroutines 的编译器支持情况。

本文使用的编译器是 GCC 10.5.0,对应的编译指令为:

g++ -fcoroutines -std=c++20

2 协程返回类型

2.1 示例代码

下面是在 协程1-并发基础概念 中提到的“斐波那契数列生成器”的完整代码,其中除了之前提到的 main 函数和 fibo 协程函数,还包括协程返回类型 generator 的定义。

// 编译指令:g++ -fcoroutines -std=c++20 demo_gen.cc
#include <coroutine>
#include <iostream>
#include <stdexcept>

template <typename T>
struct generator {
  // 类名 `generator` 不是必须的,可以使用 'MyGenerator' (或者任何别的名字)替代,
  // 同时在嵌套类 `promise_type` 中包含 'MyGenerator get_return_object()' 方法。

  struct promise_type;
  using handle_type = std::coroutine_handle<promise_type>;

  struct promise_type {
    T value_;                       // 缓存协程执行结果
    std::exception_ptr exception_;  // 缓存协程执行异常

    generator get_return_object() { return generator(handle_type::from_promise(*this)); }
    std::suspend_never initial_suspend() noexcept { return {}; }  // 如果计算复杂,或考虑异常,建议返回 suspend_always
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { exception_ = std::current_exception(); }  // 保存异常

    template <std::convertible_to<T> From>  // C++20 concept
    std::suspend_always yield_value(From&& arg) {
      value_ = std::forward<From>(arg);  // 在协程内部执行,将结果缓存到 promise 中
      return {};
    }
    void return_void() {}
  };

  generator() = default;
  generator(generator&& src) : handle_(src.handle_) { src.handle_ = nullptr; }

  generator& operator=(generator&& src) {
    if (this != &src) {
      if (handle_) {
        handle_.destroy();
      }
      handle_ = src.handle_;
      src.handle_ = nullptr;
    }
    return *this;
  }

  ~generator() {
    if (handle_) {
      handle_.destroy();
    }
  }

 private:
  handle_type handle_;
  generator(handle_type handle) : handle_(handle) {}

  void check() {
    if (!handle_) {
      throw std::logic_error("coroutine handle is null");
    }
  }

 public:
  T value() {
    check();
    return std::move(handle_.promise().value_);  // 在协程外部执行,从 promise 中“移走”结果
  }

  bool done() {
    check();
    return handle_.done();
  }

  void resume() {
    check();
    handle_.resume();
    if (handle_.promise().exception_) {
      std::rethrow_exception(handle_.promise().exception_);
    }
  }
};

// 协程函数,生成斐波那契数列 1,1,2,3,5,8...
generator<unsigned> fibo(int n) {
  unsigned a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    co_yield b;  // 每次生成 1 个数值,挂起,等待恢复
    auto next = a + b;
    a = b;
    b = next;
  }
  co_return;  // 可省略
}

int main() {
  constexpr int count = 6;
  auto f = fibo(count);                   // 调用 fibo 协程
  for (; !f.done(); f.resume()) {         // 如果没有完成,恢复执行
    std::cout << f.value() << std::endl;  // 每次获得 1 个数值
  }
}

2.2 返回类型结构

示例代码中的协程返回类型定义包括三部分:返回类型 generator<T>、承诺类型 generator::promise_type、协程句柄 coroutine_handle<promise_type>

继承
关联
依赖
关联
coroutine_hand1e<void>
# void* _M_frame_ptr
+coroutine_handle()
+coroutine_handle(nullptr_t)
+operator=(nullptr_t)
+address()
+from_address(void*)
+operator bool()
+done()
+operator()
+resume()
+destroy()
coroutine_handle<promise_type>
+from_promise(promise_type&)
+promise()
generator<T>
- coroutine_handle<promise_type> handle_
+generator()
+generator(generator&&)
+operator=(generator&&)
+~generator()
-generator(coroutine_handle<promise_type>)
-check()
+value()
+done()
+resume()
promise_type
+ T value_
+ exception_ptr exception_
+get_return_object()
+initial_suspend()
+final_suspend()
+unhandled_exception()
+yield_value(From&& arg)
+return_void()

2.2.1 协程句柄

为了在恢复协程时,可以从挂起时的状态继续执行,通常会在堆中创建协程帧,其中保存了协程恢复和协程销毁两个函数指针,也保存了协程实参、内部状态和局部变量等数据,其中承诺(Promise)对象的类型 promise_type 需要自定义实现。

协程句柄

协程句柄 coroutine_handle 是标准库提供的模板类,它封装了协程帧的指针,并提供恢复协程、销毁协程,以及获取协程的 Promise 对象等接口。
协程句柄用于在“协程外部”进行操作,并且不带所有权(non-owning),可以自由的进行转移。

// 头文件:include/c++/10.5.0/coroutine
template <typename _Promise = void>
struct coroutine_handle;  // 类模板

template <>
struct coroutine_handle<void> // 类型擦除版本,忽略 Promise 对象,只关注协程的恢复、销毁以及句柄的传递等
{
  constexpr coroutine_handle() noexcept;                    // 默认构造函数,协程帧指针初始化为空
  constexpr coroutine_handle(std::nullptr_t __h) noexcept;  // 带参构造函数,通过参数初始化协程帧指针为空
  coroutine_handle& operator=(std::nullptr_t) noexcept;     // 将协程帧指针赋值为空(nullptr)

  constexpr void* address() const noexcept;                            // 获取协程帧地址/指针
  constexpr static coroutine_handle from_address(void* __a) noexcept;  // 通过协程帧指针创建协程句柄

  constexpr explicit operator bool() const noexcept;  // 判断协程帧指针是否为空
  bool done() const noexcept;                         // 判断协程是否执行完成

  void operator()() const;  // 恢复协程执行,内部调用 resume()
  void resume() const;      // 对一个挂起后的协程进行恢复,在该协程再次挂起后,resume 接口得以返回
  void destroy() const;     // 销毁协程帧

 protected:
  void* _M_frame_ptr;  // 协程帧指针
};

template <typename _Promise> // 携带 Promise 类型信息
struct coroutine_handle : coroutine_handle<>
{
  using coroutine_handle<>::coroutine_handle;
  static coroutine_handle from_promise(_Promise& p);  // 为 _Promise 对象创建协程帧,通过协程帧指针构造协程句柄
  coroutine_handle& operator=(std::nullptr_t) noexcept;  // 将协程帧指针赋值为空(nullptr)

  constexpr static coroutine_handle from_address(void* __a) noexcept;  // 通过协程帧指针创建协程句柄
  _Promise& promise() const;                                           // 获取协程帧所对应的 _Promise 对象
};

2.2.2 承诺对象

在计算机科学中,Promise(承诺)概念用于描述一个未知值的对象,生产者可以通过 Promise 对象提供一个值(如示例代码中的 promise_type::yield_value()),而消费者通过对应的 Future 对象来获取将来的值(如示例代码中的 generator::value())。

在默认情况下,编译器会查找协程返回类型 R 的嵌套类型成员 R::promise_type 作为 Promise,这个类型需要实现几个特定的接口:

序号方法/属性是否必需说明
1T value_缓存协程执行结果
2exception_ptr exception_缓存协程执行异常信息
3R get_return_object()构造外部类型 R 的实例(协程的返回值),该实例将在第一次挂起后返回给协程的调用者。
4awaiter initial_suspend()自定义在执行协程的第一行代码之前是否需要挂起协程,如果挂起,后续可以通过协程句柄恢复,然后开始执行第一行代码,或者也可以在挂起后销毁协程。
5awaiter final_suspend() noexcept自定义在协程结束之前是否需要挂起,如果挂起,此时 coroutine_handle::done() 接口为真,后续只能通过协程句柄去手动销毁;如果不挂起,将自动销毁协程。
6void unhandled_exception()处理协程体内发生的异常,一般会通过调用 std::current_exception() 来获取并存储异常的副本,以便后续当程序从 Future 取值时进行重抛。
7awaiter yield_value(From&& arg)co_yield相关自定义 co_yield 的行为, co_yield v 会被转换成 co_await promise.yield_value(v),通常生成器使用这个接口产生一个值,并将控制权返回给它的调用者或恢复者。
8void return_void()
void return_value(Arg)
二选一自定义 co_return;co_return expr; 的行为, 编译器将其转换成对 return_void()return_value(expr) 接口的调用;此时协程即将结束它的生命周期。
9template <typename Obj>
promise_type(Obj&&, Args...)
promise_type(Args...)
promise_type()
不同参数类型的构造函数。
如果协程形参与 promise_type 构造函数一致,那么将会使用重载形参后的构造函数,否则使用默认的构造函数。
对于成员函数/lambda,第一个参数类型需要与 class/lambda 类型一致。
10awaitable await_transform(expr)自定义协程体内的 co_await 表达式,co_await 表达式会首先通过这个接口来得到 awaitable 对象,然后再将 awaitable 对象转换为 awaiter 对象。

部分方法的返回类型 awaiter,是 co_await 运算符对应的操作数类型,会在 3 co_await 表达式 部分,进行展开说明。
示例代码中使用的 std::suspend_neverstd::suspend_always标准库提供的两个 awaiter,分别代表“不挂起”和“挂起”协程。

Promise 类型能够让程序定制一个协程在初始化时和结束前的行为,以及协程体内的 co_awaitco_yieldco_return 表达式的行为。
Promise 对象用于在“协程内部”进行操作,协程通过此对象提交其结果和异常信息。

2.2.3 返回类型

返回类型 generator<T> 成员说明:

序号成员是否必需说明
0generator::promise_type协程返回类型需要包含嵌套的 promise_type 类型定义,其它成员可以根据实际需要实现。
如果无法为已存在的类型添加类型成员,可以通过为 coroutine_traits 元函数提供特化版本来扩展其 promise_type
1generator()默认构造函数,协程句柄初始为空
2generator(generator&&)移动构造函数,通过移动语义来保证协程句柄的安全传递
3generator& operator=(generator&&)移动赋值运算符,同上
4~generator()通过析构函数释放协程句柄,因为 promise_type::final_suspend() 返回的是 std::suspend_always
5handle_type handle_协程句柄
6generator(handle_type)参数为协程句柄的构造函数,用于通过 Promise 对象 promise_type::get_return_object() 创建协程返回对象
7void check()检查协程句柄的有效性
8T value()获取协程执行结果
9bool done()判断协程是否完成执行
10void resume()恢复协程执行

协程返回类型 generator<T>,不仅可以通过 value() 等接口访问协程数据,还会管理协程的生命周期,通过析构函数释放协程句柄,是一个典型的 RAII 类。

2.3 生成器处理流程

2.3.1 编译转换代码

在协程函数体及返回类型符合要求后,编译器会对协程进行代码转换。本节整理了通过 cppinsights.io 得到的转换后的代码(仅为示意)。

/*************************************************************************************
 * NOTE: The coroutine transformation you've enabled is a hand coded transformation! *
 *       Most of it is _not_ present in the AST. What you see is an approximation.   *
 *************************************************************************************/

C++ Insights 是一个基于 clang 的代码转换工具,可以查看编译器对代码进行的一些转换(transformation)。本文所使用的版本为:cpp-insights 17.0(Commits on Dec 6, 2023)、Ubuntu Clang 17.0.2。

转换前的协程代码:

generator<unsigned> fibo(int n) {
  unsigned a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    co_yield b;
    auto next = a + b;
    a = b;
    b = next;
  }
  co_return;
}

转换后的代码分为四部分,为别为:

  1. 协程帧定义:struct __fiboFrame;
  2. 协程函数:generator<unsigned int> fibo(int n);
  3. 协程恢复函数:void __fiboResume(__fiboFrame *__f);
  4. 协程销毁函数:void __fiboDestroy(__fiboFrame *__f);

2.3.1.1 协程帧定义

struct __fiboFrame {
  void (*resume_fn)(__fiboFrame *);   // 指向协程恢复函数 __fiboResume 的函数指针
  void (*destroy_fn)(__fiboFrame *);  // 指向协程销毁函数 __fiboDestroy 的函数指针
  std::__coroutine_traits_impl<generator<unsigned int> >::promise_type __promise; // 协程的 promise 对象
  int __suspend_index;                    // 记录协程当前挂起点/恢复点的索引
  //bool __initial_await_suspend_called;  // TODO: 将其修改为 __initial_await_resume_called,正确性待确认
  bool __initial_await_resume_called;     // 标记是否已完成 __promise.initial_suspend() 的执行
  int n;                                  // 协程函数参数
  unsigned int a;                         // 协程局部变量(具有自动存储期的变量,实际为另外的保存方式)
  unsigned int b;                         // 协程局部变量
  int i;                                  // 协程局部变量
  unsigned int next;                      // 协程临时变量
  std::suspend_never __suspend_81_21;     // __promise.initial_suspend() 返回的 awaiter
  std::suspend_always __suspend_84_5;     // __promise.yield_value() 返回的 awaiter
  std::suspend_always __suspend_81_21_1;  // __promise.final_suspend() 返回的 awaiter
};

2.3.1.2 协程函数

/* 创建一个协程帧对象 __f,初始化其成员变量,
   然后调用协程的恢复函数 __fiboResume(),并在协程第一次挂起时,返回协程的返回对象 */
generator<unsigned int> fibo(int n) {
  /* Allocate the frame including the promise. 分配包含 promise 的协程帧内存空间 */
  /* Note: The actual parameter new is __builtin_coro_size */
  __fiboFrame *__f = reinterpret_cast<__fiboFrame *>(operator new(sizeof(__fiboFrame)));
  __f->__suspend_index = 0;
  __f->__initial_await_resume_called = false;
  __f->n = std::forward<int>(n);  // 按值传递的形参被移动或复制,按引用传递的形参保持为引用

  /* Construct the promise. 调用 promise 对象的构造函数 */
  new (&__f->__promise) std::__coroutine_traits_impl<generator<unsigned int> >::promise_type{};

  /* Forward declare the resume and destroy function. 前向声明 */
  void __fiboResume(__fiboFrame * __f);
  void __fiboDestroy(__fiboFrame * __f);

  /* Assign the resume and destroy function pointers. 赋值函数指针 */
  __f->resume_fn = &__fiboResume;
  __f->destroy_fn = &__fiboDestroy;

  /* Call the made up function with the coroutine body for initial suspend.
     This function will be called subsequently by coroutine_handle<>::resume()
     which calls __builtin_coro_resume(__handle_)
     调用包含协程函数体的 __fiboResume() 函数,执行 __promise.initial_suspend();
     后续在执行 coroutine_handle<>::resume() 时,也会调用 __fiboResume() 函数 */
  __fiboResume(__f);

  return __f->__promise.get_return_object();  // 获取协程返回对象,并将其返回
}

2.3.1.3 协程恢复函数

/* This function invoked by coroutine_handle<>::resume()
   根据协程帧对象的 __suspend_index 值执行协程的挂起和恢复操作,
   包括执行协程的初始挂起和恢复、循环中的挂起和恢复,以及结束挂起;
   同时,还处理了协程的返回,和执行过程中可能出现的异常情况。
 */
void __fiboResume(__fiboFrame *__f)  // 未对 co_await 进行转换
{
    try
    {
        /* Create a switch to get to the correct resume point
           根据协程帧对象 __f 的 __suspend_index 值跳转到正确的恢复点 */
        switch (__f->__suspend_index) {
            case 0:                    // 执行 __promise.initial_suspend(); 可能会挂起
                break;
            case 1:                    // 恢复 __promise.initial_suspend();
                goto __resume_fibo_1;  // 执行协程函数体,包括执行 __promise.yield_value() 后挂起
            case 2:
                goto __resume_fibo_2;  // 恢复 __promise.yield_value(),继续进行协程函数体的循环处理
        }

        co_await __f->__promise.initial_suspend();  // 执行协程的初始挂起操作
                                                    // 设置 __f->__suspend_index = 1;
                                                    // 设置 __f->__initial_await_resume_called = true;
    __resume_fibo_1:
        // 协程函数体开始
        __f->a = 0;
        __f->b = 1;
        for (__f->i = 0; __f->i < __f->n; __f->i++) {
            co_await __f->__promise.yield_value<unsigned int &>(__f->b);  // 转换 co_yield __f->b;
                                                                          // 设置 __f->__suspend_index = 2;
        __resume_fibo_2:
            __f->next = (__f->a + __f->b);
            __f->a = __f->b;
            __f->b = __f->next;
        }

        __f->__promise.return_void();  // 转换 co_return;
        goto __final_suspend;          // 当协程通过 co_return 返回时,会转到 __final_suspend
        // 协程函数体结束
    }
    catch (...) 
    {
        if (!__f->__initial_await_resume_called) {  // 如果在协程函数体开始前抛出异常,
            throw;                                  // 会销毁协程,释放协程帧内存,并将异常抛回给它的调用者?
        }
        __f->__promise.unhandled_exception();  // 如果在协程函数体内抛出异常,通过 __promise 进行异常处理
    }

__final_suspend:
    co_await __f->__promise.final_suspend();  // 执行协程的结束挂起操作

    ;  // 结束协程执行
}

关于 co_await 部分的代码转换,会在 3 co_await 表达式 部分,进行展开说明。

2.3.1.4 协程销毁函数

/* This function invoked by coroutine_handle<>::destroy()
   销毁协程帧对象 __f,包括其中的变量,并释放内存空间 */
void __fiboDestroy(__fiboFrame *__f) {
  /* destroy all variables with dtors. 销毁所有具有析构函数的变量,
     包括:Promise 对象、协程实参、协程局部变量等 */
  __f->~__fiboFrame();
  /* Deallocating the coroutine frame. 释放协程帧对象的内存空间 */
  /* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
  operator delete(static_cast<void *>(__f));
}

2.3.2 执行时序

参考编译器对协程代码的转换情况,整理生成器的执行时序如下:

生成器执行时序

2.4 补充示例

本节通过示例 fibo2() 完善协程函数,扩展多处执行 co_yieldco_return 的情况,以及异常处理情况。

// 协程函数,生成斐波那契数列 0,1,1,2,3,5,8...
generator<unsigned> fibo2(int n) {
  if (n == 0) co_return;  // 直接结束协程

  co_yield 0;             // 将 0 缓存到 promise 后,挂起
  if (n == 1) co_return;  // 恢复执行,如果 n 为 1,结束协程

  co_yield 1;             // 将 1 缓存到 promise 后,挂起
  if (n == 2) co_return;  // 恢复执行,如果 n 为 2,结束协程

  unsigned a = 0, b = 1;
  for (int i = 2; i < n; i++) {
    auto s = a + b;
    if (s < b) {  // 元素溢出,抛出异常,触发 promise_type::unhandled_exception()
      throw std::runtime_error(std::string("fib(") + std::to_string(i) + std::string(") overflowed"));
    }
    co_yield s;  // 将 s 缓存到 promise 后,挂起
    a = b;       // 恢复执行,更新变量后进行下一轮计算,或结束协程
    b = s;
  }
}

int main() {
  try {
    auto f = fibo2(100);  // 测试异常情况,最大值应为 48,避免 unsigned(uint32_t) 溢出
    for (int i = 0; !f.done(); f.resume(), i++) {
      std::cout << "fib(" << i << ") = " << f.value() << '\n';
    }
  } catch (const std::exception& ex) {
    std::cout << "Exception: " << ex.what() << '\n';
  } catch (...) {
    std::cout << "Unknown exception.\n";
  }
}

3 co_await 表达式

3.1 co_await 分析

await 也被称为 asynchronous wait,即异步等待,在等待异步执行结果的过程中,让出当前线程的控制权。

在语句 auto result = co_await expr; 中,主要包含三部分:

  1. 一元运算符 co_await,可以被重载;
  2. 操作数 expr,是一个表达式,可能会经过多步转换,最终为 awaiter 类型;
  3. 返回结果 result,是 awaiter::await_resume() 的返回值。

3.1.1 表达式转换

编译器会对 co_await expr 表达式中的 expr 进行两步转换,以便对表达式的语义进行定制,并得到最终的 awaiter 对象。

  1. 转换得到 awaitable 对象:
    • 检查协程 Promise 对象是否存在成员函数 await_transform
    • 如果存在,则令 awaitable 对象为 await_transform(expr) 调用后的结果;
    • 如果不存在,则令 awaitable 对象为 expr
  2. 转换得到 awaiter 对象:
    • 检查 awaitable 对象是否重载了 operator co_await() 运算符,
    • 如果有重载,则令 awaiter 对象为重载函数调用后的结果;
    • 如果无重载,则令 awaiter 对象为 awaitable 对象(可能就是最初的 expr)。

3.1.2 awaiter 对象

awaiter 类型需要程序提供如下三个接口的实现:

序号接口是否必需说明
1bool await_ready();判断当前协程是否需要在此处挂起,如果挂起,会执行 await_suspend;否则(同步操作或结果已就绪),执行 await_resume 获得结果。
2void await_suspend(coroutine_handle<>);
bool await_suspend(coroutine_handle<>);
coroutine_handle<> await_suspend(coroutine_handle<>);
三选一通常程序可以在此处发起一个异步操作,然后返回协程的调用者或恢复者;在完成异步操作后,对当前协程句柄执行 resume 即可恢复协程的执行。
1)返回 void 类型,协程保持挂起;
2)返回 bool 类型,如果值为 true,则协程保持挂起,否则,直接恢复当前协程,即,继续执行 await_resume 及其后续代码;
3)返回 coroutine_handle 类型,协程保持挂起,同时恢复它所返回的协程句柄(可能导致当前协程恢复)。
如果 await_suspend 抛出异常,协程也会恢复。
3auto await_resume();在协程恢复时执行,其返回值将作为整个 co_await 表达式的值。

awaiter 类型能够让程序定制 co_await 表达式的语义,控制当前协程是否挂起,挂起后需要执行哪些逻辑供将来恢复,以及恢复后如何产生该表达式的值。
通过定义不同的 awaiter,就可以对各种异步操作进行协程化实现。

3.1.3 标准库 awaiter

C++ 标准库中提供了两个最基本的 awaiter,分别为:

  • suspend_always,通过 await_ready() 接口返回 false,挂起当前协程;
  • suspend_never,通过 await_ready() 接口返回 true,不挂起当前协程。
// 头文件:include/c++/10.5.0/coroutine
struct suspend_always {
  constexpr bool await_ready() const noexcept { return false; }
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};

struct suspend_never {
  constexpr bool await_ready() const noexcept { return true; }
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};

3.1.4 co_await 处理流程

co_await 表达式的处理流程示意:

co_await 处理流程

3.1.5 fibo 的 co_await 转换

在示例代码 fibo 中,使用的都是上述标准库中的 awaiter。对 __fiboResume()co_await 进行转换后,代码如下:

void __fiboResume(__fiboFrame *__f)  // 对 co_await 进行转换
 {
    try 
    {
        switch (__f->__suspend_index) {
            case 0: break;
            case 1: goto __resume_fibo_1;
            case 2: goto __resume_fibo_2;
        }

        /* co_await insights.cpp:81
           对 co_await __f->__promise.initial_suspend(); 进行转换 */
        __f->__suspend_81_21 = __f->__promise.initial_suspend();  // 获取 awaiter,赋值给 __suspend_81_21
        if (!__f->__suspend_81_21.await_ready()) {                // 如果没有 ready,则挂起,然后执行 await_suspend() 并返回
            __f->__suspend_81_21.await_suspend(
              std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
              .operator std::coroutine_handle<void>());    // TODO: 不应该执行 resume()?
            __f->__suspend_index = 1;                      // 更新下次执行时的恢复点索引
            //__f->__initial_await_suspend_called = true;  // TODO: 实现方式待确认
            return;
        }

    __resume_fibo_1:
        __f->__suspend_81_21.await_resume(); // __promise.initial_suspend() 恢复执行,在下次挂起/协程结束时返回调用者/恢复者
        __f->__initial_await_resume_called = true;

        __f->a = 0;
        __f->b = 1;
        for (__f->i = 0; __f->i < __f->n; __f->i++) {

            /* co_yield insights.cpp:84
               对 co_await __f->__promise.yield_value<unsigned int &>(__f->b); 进行转换 */
            __f->__suspend_84_5 = __f->__promise.yield_value<unsigned int &>(__f->b); // 每次循环,都会创建新的 awaiter
            if (!__f->__suspend_84_5.await_ready()) {
                __f->__suspend_84_5.await_suspend(
                  std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
                  .operator std::coroutine_handle<void>());  // TODO: 同 __suspend_81_21.await_suspend()
                __f->__suspend_index = 2;
                return;
            }

        __resume_fibo_2:
            __f->__suspend_84_5.await_resume();
            __f->next = (__f->a + __f->b);
            __f->a = __f->b;
            __f->b = __f->next;
        }

        /* co_return insights.cpp:89 */
        __f->__promise.return_void();
        goto __final_suspend;
    }
    catch (...) 
    {
        if (!__f->__initial_await_resume_called) {
            throw;
        }
        __f->__promise.unhandled_exception();
    }

__final_suspend:

    /* co_await insights.cpp:81
       对 co_await __f->__promise.final_suspend(); 进行转换 */
    __f->__suspend_81_21_1 = __f->__promise.final_suspend();
    if (!__f->__suspend_81_21_1.await_ready()) {  // 如果在这里挂起,后续只能通过协程句柄手动销毁,并且不能再次恢复协程,
        __f->__suspend_81_21_1.await_suspend(     // 并且协程句柄的 done() 接口将返回 true
          std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
          .operator std::coroutine_handle<void>());  // TODO: 同 __suspend_81_21.await_suspend()
        // TODO: 需要 return; ?
    }

    ; // TODO: 转换代码未完成?
      // 可能需要调用 __fiboDestroy(__f); 即,如果 __promise.final_suspend() 没有挂起,将自动销毁协程
}

3.2 示例代码

通过 co_await 执行异步任务:

// 编译指令:g++ -fcoroutines -std=c++20 -lpthread demo_task.cc
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>

struct task {
  struct promise_type {
    int value_;
    task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; }
    auto initial_suspend() { return std::suspend_never{}; }
    auto final_suspend() noexcept { return std::suspend_always{}; }
    void return_value(int value) { value_ = value; }
    void unhandled_exception() { std::terminate(); }
  };
  std::coroutine_handle<promise_type> h_;
  task(std::coroutine_handle<promise_type> h) : h_(h) {}
  ~task() { h_.destroy(); }
};

auto task_proc(int in_data, std::jthread& new_thd) {
  struct awaiter {
    int data_;
    std::jthread* pthd_;

    bool await_ready() { return false; }
    auto await_resume() { return data_; }

    void await_suspend(std::coroutine_handle<> h) {
      std::jthread& thd = *pthd_;
      if (thd.joinable()) throw std::runtime_error("Output jthread parameter not empty");

      thd = std::jthread([h, this] {
        std::this_thread::sleep_for(std::chrono::seconds(3));  // 模拟耗时处理
        data_ += 1000;
        h.resume();
      });
      // 不能使用 pthd_->get_id(),因为在新线程中执行 h.resume() 后,*this 会被销毁
      std::cout << "New thread ID: " << thd.get_id() << '\n';
    }
  };
  return awaiter{in_data, &new_thd};
}

task coro_task(int in_data, std::jthread& new_thd) {
  std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
  auto result = co_await task_proc(in_data, new_thd);
  // awaiter destroyed here
  std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
  co_return result;
}

int main() {
  std::jthread new_thd;
  int in_data = 10;
  auto t = coro_task(in_data, new_thd);
  while (!t.h_.done()) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "waiting..." << '\n';
  }
  std::cout << "result: " << t.h_.promise().value_ << '\n';
}

3.2.1 编译转换代码

以下是通过 cppinsights.io 获得的,未做修改和注释的,转换后的协程代码:
在语句 auto result = co_await task_proc(in_data, new_thd); 中,awaiter __suspend_45_26 的返回值被保存在了 __suspend_45_26_res 中。

/*************************************************************************************
 * NOTE: The coroutine transformation you've enabled is a hand coded transformation! *
 *       Most of it is _not_ present in the AST. What you see is an approximation.   *
 *************************************************************************************/
struct __coro_taskFrame
{
  void (*resume_fn)(__coro_taskFrame *);
  void (*destroy_fn)(__coro_taskFrame *);
  std::__coroutine_traits_impl<task>::promise_type __promise;
  int __suspend_index;
  bool __initial_await_suspend_called;
  int in_data;
  std::jthread & new_thd;
  int result;
  std::suspend_never __suspend_43_6;
  awaiter __suspend_45_26;
  int __suspend_45_26_res;
  std::suspend_always __suspend_43_6_1;
};

task coro_task(int in_data, std::jthread & new_thd)
{
  /* Allocate the frame including the promise */
  /* Note: The actual parameter new is __builtin_coro_size */
  __coro_taskFrame * __f = reinterpret_cast<__coro_taskFrame *>(operator new(sizeof(__coro_taskFrame)));
  __f->__suspend_index = 0;
  __f->__initial_await_suspend_called = false;
  __f->in_data = std::forward<int>(in_data);
  __f->new_thd = std::forward<std::jthread &>(new_thd);
  
  /* Construct the promise. */
  new (&__f->__promise)std::__coroutine_traits_impl<task>::promise_type{};
  
  /* Forward declare the resume and destroy function. */
  void __coro_taskResume(__coro_taskFrame * __f);
  void __coro_taskDestroy(__coro_taskFrame * __f);
  
  /* Assign the resume and destroy function pointers. */
  __f->resume_fn = &__coro_taskResume;
  __f->destroy_fn = &__coro_taskDestroy;
  
  /* Call the made up function with the coroutine body for initial suspend.
     This function will be called subsequently by coroutine_handle<>::resume()
     which calls __builtin_coro_resume(__handle_) */
  __coro_taskResume(__f);
  
  return __f->__promise.get_return_object();
}

/* This function invoked by coroutine_handle<>::resume() */
void __coro_taskResume(__coro_taskFrame * __f)
{
  try 
  {
    /* Create a switch to get to the correct resume point */
    switch(__f->__suspend_index) {
      case 0: break;
      case 1: goto __resume_coro_task_1;
      case 2: goto __resume_coro_task_2;
    }
    
    /* co_await insights.cpp:43 */
    __f->__suspend_43_6 = __f->__promise.initial_suspend();
    if(!__f->__suspend_43_6.await_ready()) {
      __f->__suspend_43_6.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
      __f->__suspend_index = 1;
      __f->__initial_await_suspend_called = true;
      return;
    } 
    
  __resume_coro_task_1:
    __f->__suspend_43_6.await_resume();
    std::operator<<(std::operator<<(std::operator<<(std::cout, "Coroutine started on thread: "), std::this_thread::get_id()), '\n');
    
    /* co_await insights.cpp:45 */
    __f->__suspend_45_26 = task_proc(__f->in_data, __f->new_thd);
    if(!__f->__suspend_45_26.await_ready()) {
      __f->__suspend_45_26.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
      __f->__suspend_index = 2;
      return;
    } 
    
  __resume_coro_task_2:
    __f->__suspend_45_26_res = __f->__suspend_45_26.await_resume();
    __f->result = __f->__suspend_45_26_res;
    std::operator<<(std::operator<<(std::operator<<(std::cout, "Coroutine resumed on thread: "), std::this_thread::get_id()), '\n');
    
    /* co_return insights.cpp:48 */
    __f->__promise.return_value(__f->result);
    goto __final_suspend;
  } 
  catch(...) 
  {
    if(!__f->__initial_await_suspend_called) {
      throw ;
    }   
    __f->__promise.unhandled_exception();
  }
  
__final_suspend:
  
  /* co_await insights.cpp:43 */
  __f->__suspend_43_6_1 = __f->__promise.final_suspend();
  if(!__f->__suspend_43_6_1.await_ready()) {
    __f->__suspend_43_6_1.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
  } 
  
  ;
}

/* This function invoked by coroutine_handle<>::destroy() */
void __coro_taskDestroy(__coro_taskFrame * __f)
{
  /* destroy all variables with dtors */
  __f->~__coro_taskFrame();
  /* Deallocating the coroutine frame */
  /* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
  operator delete(static_cast<void *>(__f));
}

参考

  1. 罗能著.C++20高级编程.机械工业出版社.2022.
  2. cppreference:Coroutines (C++20)
  3. David Mazières: My tutorial and take on C++20 coroutines February, 2021

宁静以致远,感谢 King 老师。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值