首页/C++20四大特性之二:coroutines特性详解/

C++20四大特性之二:coroutines特性详解

2022-01-21
收藏

MCtalk 技术文章/2021.12.02 文|马建亭 网易云信资深 C++ 开发工程师


导读

本文通过三个可运行的完整示例来体验 C++20 中的协程:coroutine。

全文共三部分,第一部分从概念上讨论协程与普通函数的区别;第二部分通过两个完整的协程代码示例,并深入到编译器层面,深入解析 promise_type 及其工作流程;第三部分介绍 co_await 的作用以及工作原理,该部分是本文最难理解的部分。

什么是 C++ 的协程?

  • 从语法角度讲, 函数代码中含有 co_await、co_yield、co_return 中任何一个关键字,这个函数就是一个协程。

  • 从系统角度讲,协程是运行在线程中的一堆代码,这些代码的执行过程可以被中断、恢复,从而实现单线程下的异步。 这点是协程异步跟多线程异步的根本区别。在多线程异步的设计中,代码挂起意味着运行堆栈的保存与 CPU 等硬件资源的调度。保存与调度由系统负责,一个线程有且只有一个运行堆栈,线程恢复时,从上次挂起的地方继续执行。在协程中,代码的“挂起”与硬件资源的调度不再挂钩:我们挂起一段协程,线程继续运行,CPU 等硬件资源不会被剥夺。

  • 从执行流程的角度讲,调用一个普通函数,只有两个状态

    调用(invoke)=> 终止(finalize)。

    调用一个协程,有四种状态:

    调用(invoke)=> 挂起(suspends)<=> 恢复(resume)=> 终止(finalize)。

在非协程的情况下, 同一线程下,调用一个函数,一定是从函数第一行开始执行,执行流程要想返回到它的调用者,也只有一个方式:return(不考虑异常),函数 return 之后,假如再次调用函数,依然一定是从函数的第一行开始执行。

协程的情况下, 调用一个协程函数,这个协程函数可能挂起多次、恢复多次,协程可以通过 co_yeild 挂起且向调用者返回一个值,下次调用从上次返回的语句下方继续执行。

图片

挂起协程时需要保存代码的调用状态、内部变量的值

保存在哪里呢?

谁来保存?

谁来恢复?

一个协程可能返回多次值给 caller,这些“返回值”如何传递?(非协程代码通过 return 机制,比如放到 eax 寄存器中)

带着上面灵魂三问,我们正式进入 C++20 的 coroutines 的世界。

协程帧、promise_type 、future_type 与 coroutine_handle

首先牢记两个前提:

  • C++20 的协程没有协程调度器,协程的挂起、恢复,由编译器安插代码完成。
  • C++20 提供了协程机制,而不是提供协程库,基础库开发者可以使用协程机制实现自己的协程库。

先看一下与协程相关的三个关键字:co_await、co_yield 与 co_return

  • co_yield some_value:保存当前协程的执行状态并挂起,返回some_value给调用者
  • co_await some_awaitable:如果 some_awaitable 没有 ready,就保存当前协程的执行状态并挂起
  • co_return some_value:彻底结束当前协程,返回 some_value 给协程调用者

我们根据 C++20 标准来实现一个最简单的、可运行的、没有返回值的协程(运行环境:https://godbolt.org/ 编译器版本选择”x86-64 gcc (coroutines)“)

#include<iostream>
#include<coroutine>
struct future_type{    
    struct promise_type;    
    using co_handle_type = std::coroutine_handle<promise_type>;        
    
    future_type(co_handle_type co_handle){        
        std::cout<<"future_type constructor"<<std::endl;        
        co_handle_ = co_handle;    
    }
    ~future_type(){        
          std::cout<<"future_type destructor"<<std::endl;                                   co_handle_.destroy();    
    }    
    future_type(const future_type&) = delete;    
    future_type(future_type&&) = delete;

    bool resume(){        
         if(!co_handle_.done()){            
         co_handle_.resume();        
         }        
         return !co_handle_.done();    
         }
private:    
    co_handle_type co_handle_;
};
struct future_type::promise_type{   
    promise_type(){        
         std::cout<<"promise_type constructor"<<std::endl;    
    }    
    ~promise_type(){        
          std::cout<<"promise_type destructor"<<std::endl;    
    }    
    auto get_return_object(){        
          std::cout<<"get_return_object"<<std::endl;        
          return co_handle_type::from_promise(*this);    
    }    
    auto initial_suspend(){        
          std::cout<<"initial_suspend"<<std::endl;        
          return std::suspend_always();    
    }    
    auto final_suspend() noexcept(true) {        
          std::cout<<"final_suspend"<<std::endl;        
          return std::suspend_always();    
    }    
    void return_void(){        
          std::cout<<"return_void"<<std::endl;    
    }    
    void unhandled_exception(){        
          std::cout<<"unhandled_exception"<<std::endl;        
          std::terminate();    
    }
};
future_type three_step_coroutine(){    
      std::cout<<"three_step_coroutine begin"<<std::endl;    
      co_await std::suspend_always();    
      std::cout<<"three_step_coroutine running"<<std::endl;    
      co_await std::suspend_always();    
      std::cout<<"three_step_coroutine end"<<std::endl;
}
int main(){    
      future_type ret = three_step_coroutine();     
      std::cout<<"=======calling first resume======"<<std::endl;    
      ret.resume();    
      std::cout<<"=======calling second resume====="<<std::endl;    
      ret.resume();    
      std::cout<<"=======calling third resume======"<<std::endl;    
      ret.resume();    
      std::cout<<"=======main end======"<<std::endl;    
      return 0;
 }

输出为:

promise_type 
constructorget_return_object
initial_suspend
future_type constructor
=======calling first resume======
three_step_coroutine begi
n=======calling second resume=====
three_step_coroutine running
=======calling third resume======
three_step_coroutine end
return_void
final_suspend
=======main end======
future_type destructor
promise_type destructor

我们先忽略 future_type 与 promise_type 这两个莫名其妙的结构体,直接从 main 函数开始看起,第一行代码就与非协程时代的代码完全不同。

以非协程时代的认知来看:

  • three_step_coroutine 是一个函数。(因为它有着一个(),有返回值,有大括号......)
  • ret 是 three_step_coroutine 的一个返回值,它的类型是 future_type。
  • 第一行执行结束后,ret 被返回,意味着 three_step_coroutine 已经执行完了。

在协程时代,上面三条完全被推翻了,一条都不剩!

  • three_step_coroutine 不是一个函数,它是一个协程!(因为它的 body 中含有 co_await)
  • ret 并不是 three_step_coroutine 的返回值,它是协程对象的管理者,或者说,ret 是协程本身,协程的返回值在 ret 内部存储
  • 第一行结束后,three_step_coroutine 的 body 中一行代码都没有被执行!

先抛开 future_type 与 promise_type 内部细节,main 函数开始后,执行顺序如下:

  1. 编译器安插的代码负责调用 new 操作符分配一个协程帧(coroutine frame),将参数拷贝到帧中。(promise_type 可以重写 new 操作符,若无重写,此处调用全局 new 操作符。重写了 new 一般也要重写 delete)
  2. 编译器安插的代码负责构造 promise_type 的对象
  3. 获取 return_object 对象用来存储协程产生的“返回值“,return_object 会在协程第一次挂起后被用到
  4. 调用 promise 对象的 initial_suspend 方法,该方法将 three_step_cocoutine 协程挂起,执行权即将返回给 main 函数
  5. 构造 future_type 对象,并将该对象返回给 ret,执行权回到 main 函数。
  6. main 函数执行第一个 resume,resume 通过 future_type 对象中保存的协程句柄 co_handle_,将协程恢复执行,执行权从 main 函数交给 three_step_cocoutine 协程。
  7. three_step_cocoutine 协程执行第一个 cout,然后遇到 co_await 挂起自己,执行权返回给 main 函数
  8. main 函数执行第二个 resume,从略。
  9. main 函数执行,第三个 resume,协程打印出第三个 cout,然后调用 return_void 保存协程返回值到 return_object 对象中(该对象实际是promise 对象)
  10. 调用 promise 对象中的 final_suspend 函数,协程再次挂起,执行流程再次回到 main 函数。
  11. main 函数结束,future 对象、promise 对象被依次销毁,程序结束。

通过分析上面的代码以及代码的执行流程,我们得出以下几点结论:

  • main 函数不能是协程,也就是 main 函数中不能出现 co_await、co_return、co_yield 等关键字(构造函数也不能是协程)
  • 非协程代码(main 函数中的代码)可以调用协程代码
  • 非协程代码,通过协程句柄 co_handle 可以控制协程的执行。本例中,非协程代码通过调用 future 对象暴露的 resume 方法,通过 co_handle,控制协程 resume。
  • 协程第一次执行前,编译器安插的代码会负责创建 promise 对象,并调用 promise 对象的 get_return_type 方法,获得 return_object。
  • 然后调用 initial_suspend 方法,该方法的作用控制协程初始化完毕后的行为:初始化完毕后是挂起协程还是正式开始执行协程 body 代码
  • 协程第一次挂起后,创建 future 对象,并将对象返回给调用者,执行权交回到调用者。
  • 协程 body 最后一行代码执行完毕后,会调用 return_void 或者 return_value 保存返回值,然后继续调用 promise 的 final_suspend 方法,假如该方法挂起了协程,则执行权直接回到调用者;假如该方法没有挂起协程,执行完 final_suspend 内的代码后,此协程彻底执行完毕,promise 对象被销毁,执行权再回到调用者。

通过上面的几点结论,我们又能得出进一步的总结:

  • future_type、promise_type、coroutine_handle 是协程机制的主要手段
  • promise_type 是 future_type 内的类型
  • 程序员对协程的设计(一个协程返回什么类型的值、协程初始化完毕后是挂起还是继续执行、协程出异常了如何处理等等),通过 promise_type 传递给编译器,编译器负责实例化出 promise 对象
  • 编译器将协程的句柄(coroutine_handle)装填到 future 对象中,从而将协程的控制权暴露给调用者。

以下便是编译器实际安插的代码的大致样子,便是我们的 three_step_coroutine 里面的代码

{  
  co_await promise.initial_suspend();  
  try{    
    <body>  
  }catch (...){    
    promise.unhandled_exception();  
  }  
  co_await promise.final_suspend();
}

从上面的代码可以看出,协程 body 代码在被执行到之前,会先执行 initial_suspend 方法,出异常后,执行 unhandled_exception 方法,执行完毕后,执行 final_suspend 方法。promise_type 还有多个接口没在上面代码中反映出来。

我们看看 promise_type 的接口全貌:

图片

coroutine_handle 也暴露出多个接口,用于控制协程的行为、获取协程的状态,与 promise_type 不同的是,promise_type 里的接口需要我们填写实现,并且是给编译器调用的。coroutine_handle 的接口不需要我们填写实现,我们可以直接调用。

图片

有了上面的清单,我们就可以尝试实现一个返回int值的协程了(运行环境:https://godbolt.org/ 编译器版本选择”x86-64 gcc (coroutines)“):

#include<iostream>
#include<coroutine>
using namespace std;

struct future_type_int{    
      struct promise_type;    
      using co_handle_type = std::coroutine_handle<promise_type>;
      
      future_type_int(co_handle_type co_handle){                
            std::cout<<"future_type_int constructor"<<std::endl;         
            co_handle_ = co_handle;    
      }    
      ~future_type_int(){        
            std::cout<<"future_type_int destructor"<<std::endl;           
            co_handle_.destroy();    
      }    
      future_type_int(const future_type_int&) = delete;    
      future_type_int(future_type_int&&) = delete;    
      bool resume(){        
            if(!co_handle_.done()){            
                co_handle_.resume();        
            }        
            return !co_handle_.done();    
      }    
      co_handle_type co_handle_;};
struct future_type_int::promise_type{    
      int ret_val;    
      promise_type(){        
            std::cout<<"promise_type constructor"<<std::endl;    
      }    
      ~promise_type(){        
            std::cout<<"promise_type destructor"<<std::endl;    
      }    
      auto get_return_object(){        
             std::cout<<"get_return_object"<<std::endl;        
             return co_handle_type::from_promise(*this);    
      }    
      auto initial_suspend(){        
             std::cout<<"initial_suspend"<<std::endl;        
             return std::suspend_always();    
      }    
      auto final_suspend() noexcept(true) {        
             std::cout<<"final_suspend"<<std::endl;        
             return std::suspend_never();    
      }    
      void return_value(int val){        
             std::cout<<"return_value : "<<val<<std::endl;        
             ret_val = val;    
      }    
      void unhandled_exception(){        
             std::cout<<"unhandled_exception"<<std::endl;         
             std::terminate();    
      }    
      auto yield_value(int val){        
            std::cout<<"yield_value : "<<val<<std::endl;        
            ret_val = val;        
            return std::suspend_always();    
      }
};
future_type_int three_step_coroutine(){    
      std::cout<<"three_step_coroutine begin"<<std::endl;    
      co_yield 222;    
      std::cout<<"three_step_coroutine running"<<std::endl;    
      co_yield 333;    
      std::cout<<"three_step_coroutine end"<<std::endl;    
      co_return 444;
}
int main(){    
     future_type_int future_obj = three_step_coroutine();             
     std::cout<<"=======calling first resume======"<<std::endl;    
     future_obj.resume();    
     std::cout<<"ret_val = "<<future_obj.co_handle_.promise().ret_val<<std::endl;      std::cout<<"=======calling second resume====="<<std::endl;    
     future_obj.resume();    
     std::cout<<"ret_val = "<<future_obj.co_handle_.promise().ret_val<<std::endl;        
     std::cout<<"=======calling third resume======"<<std::endl;    
     future_obj.resume();    
     std::cout<<"ret_val = "<<future_obj.co_handle_.promise().ret_val<<std::endl;   
     std::cout<<"=======main end======"<<std::endl;    
     return 0;
}

值得注意的是,promise_type 是 C++20 标准指定的类型名,但 future_type 不是, 你可以把它写成任意的名称,比如 future_type_int,只要它内部有一个 promise_type 即可编译通过。

输出为:

promise_type constructor
get_return_object
initial_suspend
future_type constructor
=======calling first resume======
three_step_coroutine begin
yield_value : 222
ret_val = 222
=======calling second resume=====
three_step_coroutine running
yield_value : 333
ret_val = 333
=======calling third resume======
three_step_coroutine end
return_value : 444
final_suspend
promise_type destructor
ret_val = 444
=======main end======
future_type destructor

至此,我们便可以回答上文提到的"灵魂三问"了:

  • 协程挂起时保存的调用栈等信息,保存在协程帧中
  • 由编译器负责安插代码,进行协程帧的创建、销毁,同时负责调用栈的保存、恢复
  • 返回值由编译器传给 promise 对象,保存到 promise 对象的成员中。调用者通过 coroutin_handle 即可拿到 promise 对象,进而拿到返回值

通过上一个代码示例,我们已经能够设计协程、返回想要的值了,简单来说就是:

promise_type 实现 yield_value、return_value,协程 body 代码便可以通过 co_yield、co_return 返回值给调用者。

那 co_await 有什么用?

在返回 int 值的协程示例中,我们从头到尾都没用到过 co_await 关键字。

co_await 存在的意义在哪?

在上面的示例代码中(无返回值的协程代码),three_step_coroutine 的 body 中有这样一行代码:

co_await std::suspend_always();

为何调用了这行代码协程就挂起了?

返回 int 值的协程示例中,我们获取返回值的方式非常不优雅,那该如何改进?

co_await 将回答上面所有的疑问。

co_await 与 Awaitable 、 Awaiter

co_await 是一个一元操作符,对于代码:

co_await xxx;

co_await 是操作符,xxx 是它的操作数。只有 xxx 是 Awaitable 的类型才能做 co_await 的操作数,std::suspend_always()就是一个 Awaitable 的类型。

一个类型怎样才能成为 Awaitable?

  • 当前协程的 promise 对象中实现了 await_transform 方法,则当前协程 body 中出现的 co_await aaa; co_await bbb; 中的 aaa、bbb 都是 Awaitable 的!co_await xxx; 会被处理成 co_await promise.await_transform(xxx)。
  • xxx 对应的 future_type 实现了 await_ready、await_suspend、await_resume 三个方法,这是最常见的实现 Awaitable 的途径,std::suspend_always() 就属于这种情况。

只有 xxx 是 Awaitable 的,co_await xxx; 这样的代码在语法层面才是合法的,我们可以看出,xxx 是 Awaitable 不光取决于 xxx,也取决于调用 co_await xxx;的调用者的 promise_type 的实现(实际上,编译器会先看调用者的 promise_type 的实现,假如调用者的 promise_type 没有实现 await_transform,才会看 xxx 是否实现了 await_ready、await_suspend、await_resume 三接口)

实现了 await_ready、await_suspend、await_resume 三个接口的类,称作是 Awaiter 类型。

Awaiter 类型一定是 Awaitable 的。

非 Awaiter 类型有可能是 Awaitable 的。

Awaiter 三个接口分别是做啥用的呢?我们先看下编译器是如何处理 “co_await xxx;”这种代码的(忽略异常处理):

{    
      auto a = get_awaiter_object_of_xxx();    
      if(!a.await_ready()) {       
          <suspend_current_coroutine>    
      #if(a.await_suspend returns void)        
          a.await_suspend(coroutine_handle);            
          return_to_the_caller_of_current_coroutine();    
      #elseif(a.await_suspend returns bool)        
          bool await_suspend_result = a.await_suspend(coroutine_handle);                   if (await_suspend_result)            
              return_to_the_caller_of_current_coroutine();        
          else            
              goto <resume_current_cocourine>;//这行goto是多余的,意在强调await_suspend_result == false时的执行流程。    
      #elseif(a.await_suspend returns another coroutine_handle)        
          auto another_coro_handle = a.await_suspend(coroutine_handle);        
          another_coro_handle.resume();        
          //资料上显示,执行another_coro_handle.resume(); 后依然能将执行权返回给当前协程的caller,此处笔者怀疑不能。        
          //资料出处:https://blog.panicsoftware.com/co_awaiting-coroutines/         
          return_to_the_caller_of_current_coroutine();//故本行代码是否应该存在请保持疑问。    
       #endif        <resume_current_cocourine>    
       }    
       return a.await_resume();
 }

类似于 promise_type,程序员可以根据不同场景,实现 await_ready 等接口,用以定义 co_await 的行为。 设想一个实际场景场景会更好理解:xxx 是一个协程,我们是这个协程的作者,我们想定制别人 co_await xxx 时的行为,于是我们在 xxx 协程的 future_type 中实现了 awaitable 三接口。当编译器遇到 co_await xxx; 时,编译器会做如下处理:

  • 拿到 xxx 的 future 对象,通过 future 对象,获取到 awaiter 对象,获取的具体手法(也就是 get_awaiter_object_of_xxx 函数)此处从略,只要 future_type 实现了 awaitable 的三个接口,此处就能成功获取到 awaiter 对象。
  • 调用 await_ready 接口,假如 caller 协程等待(await)的东西我们的 xxx 协程已经准备好了,或者 caller 想等待的东西不耗时,我们的 xxx 协程可以同步返回,我们就通过本接口返回一个 true 值,编译器拿到这个 true 之后,就知道没必要挂起当前(caller)协程(挂起一个协程的消耗虽然远小于挂起一个线程,但是也是有消耗的),于是就调用 await_resume,返回 await_resume 的返回值。await_ready() 方法存在的目的是在已知操作将同步完成而无需挂起的情况下,免除  suspend_current_coroutine 操作的成本。绝大多数情况下,await_resume 接口返回的都是 false,比如std::suspend_always()。
  • 假如当前协程等待的东西,xxx 协程尚未准备好,那编译器就产生一段代码,将当前协程挂起,准备将控制权交给 xxx 协程,等待 xxx 执行,以便将 caller 想要的东西准备好。
  • 挂起当前协程后,调用 a.await_suspend,并将当前协程的句柄传入。 此时 a.await_suspend 内部就可以恢复 xxx 协程的执行了。await_suspend 函数体执行完毕后返回一个值,这个值有四种情况:
  1. await_suspend 返回void:协程执行权交还给当前协程的 caller。(也就是 xxx 的 caller 的 caller),当前协程在未来某个时机被 resume 之后,代码从开始执行,最终从 await_resume 拿到返回值。
  2. await_suspend 返回 true:同返回 void。
  3. await_suspend 返回 false:通过执行恢复当前协程,然后执行 await_resume,将产生的返回值返回给当前协程,执行权继续在当前协程。
  4. await_suspend 返回一个协程句柄:调用该协程句柄的 resume 方法,恢复对应协程的运行。resume 有可能链式反应,最终导致当前协程被 resume,甚至这个协程句柄有可能恰好直接是当前协程的句柄,则当前协程直接被 resume。
  • 在调用 await_suspend 之前,当前协程已经被完整的挂起了,所以当前协程的句柄可以在其他线程恢复, 本文暂不讨论这种复杂场景。

简单来说,对于 co_await xxx; 是这样的:

  • xxx 实现的 await_ready 告诉 xxx 的调用者,是否需要挂起调用者协程。
  • xxx 实现的 await_resume,负责将 xxx 执行过程中产生的值返回给 xxx 的调用者。
  • xxx 实现的await_suspend,负责决定 xxx 执行完毕后(有可能是中途挂起)执行权的归属。

是不是有点绕?

通过一个完整的实例就比较清晰了:

#include<iostream>
#include<coroutine>
using namespace std;
struct future_type_int{    
    struct promise_type;    
    using co_handle_type = std::coroutine_handle<promise_type>;    
    struct promise_type{        
        int ret_val;        
        promise_type(){            
            std::cout<<"promise_type constructor"<<std::endl;        
        }        
        ~promise_type(){            
            std::cout<<"promise_type destructor"<<std::endl;        
        }        
        auto get_return_object(){            
            std::cout<<"get_return_object"<<std::endl;            
            return co_handle_type::from_promise(*this);        
        }        
        auto initial_suspend(){            
            std::cout<<"initial_suspend"<<std::endl;            
            return std::suspend_always();        
        }        
        auto final_suspend() noexcept(true) {            
            std::cout<<"final_suspend"<<std::endl;            
            return std::suspend_never();        
        }        
        void return_value(int val){            
            std::cout<<"return_value : "<<val<<std::endl;            
            ret_val = val;        
        }        
        void unhandled_exception(){            
            std::cout<<"unhandled_exception"<<std::endl;            
            std::terminate();        
        }        
        auto yield_value(int val){            
            std::cout<<"yield_value : "<<val<<std::endl;            
            ret_val = val;            
            return std::suspend_always();        
        }    
    };    
    future_type_int(co_handle_type co_handle){        
        std::cout<<"future_type_int constructor"<<std::endl;        
        co_handle_ = co_handle;    
    }    
    ~future_type_int(){        
        std::cout<<"future_type_int destructor"<<std::endl;    
    }    
    future_type_int(const future_type_int&) = delete;    
    future_type_int(future_type_int&&) = delete;    
    bool resume(){        
        if(!co_handle_.done()){            
            co_handle_.resume();        
        }        
        return !co_handle_.done();    
    }    
    bool await_ready() {         
        return false;     
    }    
    bool await_suspend(std::coroutine_handle<> handle) {        
        resume();        
        return false;    
    }    
    auto await_resume() {        
        return co_handle_.promise().ret_val;    
    }    co_handle_type co_handle_;};
future_type_int three_step_coroutine(){    
    std::cout<<"three_step_coroutine begin"<<std::endl;    
    co_yield 222;    
    std::cout<<"three_step_coroutine running"<<std::endl;    
    co_yield 333;    
    std::cout<<"three_step_coroutine end"<<std::endl;    
    co_return 444;
}
struct future_type_void{    
    struct promise_type;    
    using co_handle_type = std::coroutine_handle<promise_type>;      
    future_type_void(co_handle_type co_handle){        
        std::cout<<"future_type_void constructor"<<std::endl;        
        co_handle_ = co_handle;    
    }    
    ~future_type_void(){        
        std::cout<<"future_type_void destructor"<<std::endl;           
        co_handle_.destroy();    
    }    
    future_type_void(const future_type_void&) = delete;    
    future_type_void(future_type_void&&) = delete;    
    bool resume(){        
        if(!co_handle_.done()){            
            co_handle_.resume();        
        }        
        return !co_handle_.done();    
    }    
    co_handle_type co_handle_;
};
struct future_type_void::promise_type{    
    promise_type(){        
        std::cout<<"promise_type constructor void"<<std::endl;    
    }    
    ~promise_type(){        
        std::cout<<"promise_type destructor void"<<std::endl;    
    }    
    auto get_return_object(){        
       std::cout<<"get_return_object void"<<std::endl;        
       return co_handle_type::from_promise(*this);    
    }    
    auto initial_suspend(){        
        std::cout<<"initial_suspend void"<<std::endl;        
        return std::suspend_always();    
    }    
    auto final_suspend() noexcept(true) {        
        std::cout<<"final_suspend void"<<std::endl;        
        return std::suspend_never();    
    }    
    void unhandled_exception(){        
        std::cout<<"unhandled_exception void"<<std::endl;         
        std::terminate();    
    }    
    void return_void(){        
        std::cout<<"return_void void: "<<std::endl;    
    }
};
future_type_void call_coroutine(){    
    auto future = three_step_coroutine();    
    std::cout<<"++++++++call three_step_coroutine first++++++++"<<std::endl;    
    auto val = co_await future;    
    std::cout<<"++++++++call three_step_coroutine second++++++++, val: "<<val<<std::endl;    
    val = co_await future;     
    std::cout<<"++++++++call three_step_coroutine third++++++++, val: "<<val<<std::endl;    
    val = co_await future;     
    std::cout<<"++++++++call three_step_coroutine end++++++++, val: "<<val<<std::endl;    
    co_return;
}
int main(){   
    auto ret = call_coroutine();    
    std::cout<<"++++++++begine call_coroutine resume in main++++++++"<<std::endl;    
    ret.resume();    
    std::cout<<"++++++++end call_coroutine resume in main++++++++"<<std::endl;    return 0;
}

输出:

promise_type constructor void
get_return_object void
initial_suspend void
future_type_void constructor
++++++++begine call_coroutine resume in main++++++++
promise_type constructor
get_return_object
initial_suspend
future_type_int constructor
++++++++call three_step_coroutine first++++++++
three_step_coroutine begin
yield_value : 222
++++++++call three_step_coroutine second++++++++, val: 222
three_step_coroutine running
yield_value : 333
++++++++call three_step_coroutine third++++++++, val: 333
three_step_coroutine end
return_value : 444
final_suspend
promise_type destructor
++++++++call three_step_coroutine end++++++++, val: 444
return_void void: 
future_type_int destructor
final_suspend void
promise_type destructor void
++++++++end call_coroutine resume in main++++++++

future_type_void destructor

我们可以看到,每 co_await future; 一次,就会返回一个 three_step_coroutine 的一个 co_yield 的值,我们在 future_type_int 中实现的 await_suspend 方法返回了一个 false,保证了 co_await future; 之后代码的执行权回到 call_coroutine 协程而不是 main 函数。大家可以尝试下更改 await_suspend 的返回值,有助于理解这个返回值对 co_await 的行为的影响。

到这里,大家也应该能够理解为何 co_await std::suspend_always() 能够挂起当前协程了,因为 suspend_always 的实现是这样的:

constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}

await_ready 返回 false 确保当前协程会被挂起,await_suspend 返回 void 确保当前协程挂起后,执行权回到当前协程的 caller。仅此而已。

存在的问题:

co_await 语义不统一(有可能是 invoke&await 语义,也有可能是 invoke&suspend 语义),随着协程库的实现而改变,代码阅读障碍。

至此,C++20 中协程的内容告一段落,在学习 C++ 协程的时候,很多概念需要去仔细琢磨,比如 Awaiter 与 Awaitable 的关系,搜索资料研究编译器为协程安插的代码有助于我们对这些概念的理解。水平有限,文章如有疏漏之处,欢迎大家联系讨论。

作者介绍

马建亭,网易云信资深 C++ 开发工程师,主要负责网易云信 NERTC SDK 的开发、维护、重构等工作,拥有多年 C++ 客户端开发经验,现致力于跨平台 C++ 开发。

参考文献

  • https://blog.panicsoftware.com/co_awaiting-coroutines
  • https://en.cppreference.com/w/cpp/language/coroutines
  • https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type
  • https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
© 1997-2022 网易公司增值电信业务许可证B1-20180288浙B1.B2-20090185粤B2-20090191-18工业和信息化部备案管理系统网站隐私政策