导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与宽泛开发者打造的分享交换窗口。栏目邀约腾讯技术人分享原创的技术积淀,与宽泛开发者互启迪共成长。本文作者是腾讯后盾开发工程师杨良聪。

协程(coroutine)是在执行过程中能够被挂起,在后续能够被复原执行的函数。在C++20中,当一个函数外部呈现了co_await、co_yield、co_return中的任何一个时,这个函数就是一个协程。

C++20协程的一个简略的示例代码:

coro_ret<int> number_generator(int begin, int count) {    std::cout << "number_generator invoked." << std::endl;    for (int i=begin; i<count; ++i) {        co_yield i;    }    co_return;}int main(int argc, char* argv[]){    auto g = number_generator(1, 10);    std::cout << "begin to run!" << std::endl;    while(!g.resume()) {        std::cout << "got number:" << g.get() << std::endl;    }    std::cout << "coroutine done, return value:" << g.get() << std::endl;    return 0;}

number_generator内呈现了co_yield和co_return所以这不是一个一般的函数,而是一个协程,每当程序执行到第4行co_yield i;时,协程就会挂起,程序的控制权会回到调用者那里,直到调用者调用resume办法,此时会复原到上次协程yield的中央,持续开始执行。

Promise

number_generator的返回类型是coro_ret<int>,而协程自身的代码中并没有通过return返回这个类型的数据,这就是C++20里实现协程的一个关键点: 协程的返回类型T中,必须有T::promise_type这个类型定义,这个类型要实现几个接口。还是先看代码:

//!coro_ret 协程函数的返回值,外部定义promise_type,承诺对象template <typename T>struct coro_ret{    struct promise_type;    using handle_type = std::coroutine_handle<promise_type>;    //! 协程句柄    handle_type coro_handle_;        //!promise_type就是承诺对象,承诺对象用于协程内外交换    struct promise_type    {            promise_type() {            std::cout << "promise constructor invoded." << std::endl;        }        ~promise_type() = default;        //!生成协程返回值        auto get_return_object()        {            std::cout << "get_return_object invoked." << std::endl;            return coro_ret<T>{handle_type::from_promise(*this)};        }        //! 留神这个函数,返回的就是awaiter        //! 如果返回std::suspend_never{},就不挂起,        //! 返回std::suspend_always{} 挂起        //! 当然你也能够返回其余awaiter        auto initial_suspend()        {            //return std::suspend_never{};            std::cout << "initial_suspend invoked." << std::endl;            return std::suspend_always{};        }        //!co_return 后这个函数会被调用        /*        void return_value(const T&amp; v)        {            return_data_ = v;            return;        }        */        void return_void()        {            std::cout << "return void invoked." << std::endl;        }        //!        auto yield_value(const T&amp; v)        {            std::cout << "yield_value invoked." << std::endl;            return_data_ = v;            return std::suspend_always{};            //return std::suspend_never{};        }        //! 在协程最初退出后调用的接口。        auto final_suspend() noexcept        {            std::cout << "final_suspend invoked." << std::endl;            return std::suspend_always{};        }        //        void unhandled_exception()        {            std::cout << "unhandled_exception invoked." << std::endl;            std::exit(1);        }        //返回值        T return_data_;    };    coro_ret(handle_type h)            : coro_handle_(h)    {    }    ~coro_ret()    {        //!自行销毁        if (coro_handle_)        {            coro_handle_.destroy();        }    }    //!复原协程,返回是否完结    bool resume()    {        if (!coro_handle_.done()) {  //! 如果曾经done了,再调用resume,会导致coredump            coro_handle_.resume();        }        return coro_handle_.done();    }    bool done() const    {        return coro_handle_.done();    }    //!通过promise获取数据,返回值    T get()    {        return coro_handle_.promise().return_data_;    }};

coro_ret是个自定义的构造,为了能作为协程的返回值,须要定义一个promise_type。这个类型须要实现如下的接口:

  • coro_ret<T> get_return_object() 这个接口要能用promise本人的实例结构出一个协程的返回值,会在协程正在运行前进行调用,这个接口的返回值会作为协程的返回值。
  • awaiter initial_suspend() 这个接口会在协程被创立(也就是第一次调用),真正运行前,被调用,如果这个接口返回的是std::suspend_never{},那么协程一创立进去,就会立即执行;如果返回的是std::suspend_always{},那么协程被创立进去时,会处于挂起状态,不会立即执行,须要调用者被动resume才会触发第一次执行。这两个值其实都是awaiter类型,前面再解释这个类型。
  • awaiter yield_value(T v) 这个接口会在 co_yield v 时被调用,把co_yield前面跟着的值v做为参数传入,这里个别就是把这个值保留下来,提供给协程的调用者,返回值也是awaiter,这里个别返回的是std::suspend_always{}。
  • void return_value(T v) 这个接口会在 co_return v 时被调用,把co_return前面跟着的值v作为参数传入,这里个别就是把这个值保留下来,提供给协程调用者。
  • void return_void() 如果 co_return 前面没有接任何值,那么就会调用这个接口。return_void和return_value只能抉择一个实现,否则会报编译谬误。
  • awaiter final_suspend() 在协程最初退出后调用的接口,如果返回 std::suspend_always 则须要用户自行调用coroutine_handle的destroy接口来开释协程相干的资源;如果返回std::suspend_never则在协程完结后,协程对应的handle就曾经为空,不能再调用destroy了(会coredump)
  • void unhandled_exception()如果协程内的代码抛出了异样,那么这个接口会被调用。

协程相干对象

能够看出promise类的工作次要是两个:一是定义协程的执行流程,次要接口是initial_suspend,final_suspend,二是负责协程和调用者之间的数据传递,次要接口是yield_value和return_value。

std::coroutine_handle<promise_type>是协程的管制句柄类,最重要的接口是promise、resume,前者能够取得协程的promise对象,后者能够复原协程的运行。此外还有destroy接口,用来销毁协程实例,done接口用于返回协程是否曾经完结运行。通过std::coroutine_handle<promise_type>::from_promise()办法,能够从promise实例取得对应的handle。

coro_ret中其余几个接口resume,done和get_data不是必须的,只是为了方便使用而存在。

总结一下,一个协程与这几个对象关联在一起:

  • promise
  • coroutine handle
  • coroutine state

这是个在堆上调配的外部对象,没有裸露给开发者,是用来保留协程内相干数据和状态的,具体来说就是:

  • promise对象
  • 传给协程的参数
  • 以后挂终点的相干数据
  • 生命周期逾越挂终点的长期变量和本地变量,也就是在resume后须要复原进去的变量。

协程的创立

长期总结

要在c++20里实现一个协程,须要定义一个协程的返回类型T,这个T内须要定义一个promise_type的类型,这个类型要实现几个指定的接口,这样就足够了。这样,要开发一个蕴含异步操作的协程,代码的构造大抵会是这样的:

coro_return<T> logic() {    // 发动异步操作    some_async_oper();    co_yield xxx          // 复原执行了,要先检查和取得异步操作的后果     auto result = get_async_oper_result()     do_some_thing(result)     co_return}int main() {  auto co_ret = logic();  // 循环查看异步操作是否完结  while(true) {      auto result = get_async_result();      if (result) {          // 异步操作完结了,复原协程的运行,要把后果传过来          co_ret.resume()          break;      }  }}

能够看到,在协程外部,发动异步操作和获取后果,被yield宰割为了两步,和同步代码还是有着显著的区别。这时,co_await就能够施展它的作用了,应用了co_await后的协程代码会是这样的

coro_return<T> logic() {    auto result = co_await some_async_oper();    do_some_thing(result);}

这样就和同步代码就根本没有区别了,除了这个co_await

  • co_await

co_await最常见的应用形式为auto ret=co_await expr,co_await后跟一个表达式,整个语句的执行过程有多种状况,是比较复杂的。这里形容的是简化版本,次要是简化了promise.await_transform的作用,以及awaitable对象,能够点击上面链接看残缺的形容。这里假设协程的promise_type没有实现await_transform办法。 

https://en.cppreference.com/w...

用代码表白,是这样:

 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();
  • 首先是expr求值
  • expr表达式的返回值类型(awaiter)必须实现这几个接口: await_ready、await_suspend和await_resume。
  • await_ready被调用,如果返回true,那么协程齐全不会被挂起,间接会去调用await_resume()接口,把这个接口作为await的返回值,继续执行协程。
  • 如果await_ready返回false,那么协程会被挂起,而后调用await_suspend接口,并将协程的句柄传给这个接口。留神,此时协程曾经被挂起,但控制权还没有交给调用者。
  • 如果await_suspend接口的返回类型是void,或者返回类型是bool,返回值是true,那么就将控制权交还给调用者。
  • 如果await_suspend接口返回的是false,那么协程会被resume,并接着调用await_resume,把这个接口作为await的返回值,继续执行协程。
  • 如果后面的步骤中,协程被挂起了,那么当协程被调用者resume的时候,会先调用await_resume接口,把这个接口作为await的返回值,继续执行协程。
  • co_await的例子

以封装一个socket的connect操作为例,咱们心愿能像这样在协程中去connect一个tcp地址:

coro_ret<int> connect_addr_example(io_service&amp; service, const char* ip, int16_t port){    coroutine_tcp_client client;    // 异步连贯, service是对epoll的一个封装    auto connect_ret = co_await client.connect(ip, port, 3, service);    printf("client.connect return:%d\n", connect_ret);    if (connect_ret)    {        printf("connect failed, coroutine return\n");        co_return -1;    }    do_something_with_connect(client);    co_return 0;}

那么须要做的事件是

  • 第5行中的client.connect首先发动一个异步连贯的申请(设置socket为noneblock,而后connect, 并把socket和本人的指针退出epoll),返回的类型须要是一个awaiter,也就是要实现这三个接口:await_ready、await_suspend和await_resume
  • 在await_ready中,判断连贯是否曾经建设了(某些状况下connect会立即胜利返回),或者出错了(比方给connect传了非法的参数),此时须要返回true,协程就齐全不会挂起。其余状况须要返回false,让协程挂起
  • 在await_suspend中,能够保留下传入的协程句柄,而后间接返回true。
  • 在await_resume中,判断下连贯的后果,胜利返回0,其余状况返回错误码。
  • 协程外的主循环里,应用epoll进行轮询,当对应的句柄有事件时(胜利连贯、超时、出错),就取出对应的client指针,设置好连贯的后果,并resume协程。

大抵的代码如下:

   struct connect_awaiter    {        coroutine_tcp_client& tcp_client_;        // co_await开始会调用,依据返回值决定是否挂起协程        bool await_ready(){            auto status = tcp_client_.status();            switch(status)            {            case ERROR:                printf("await_ready: status error invalid, should not suspend!\n");                return true;            case CONNECTED:                printf("await_ready: already connected, should not suspend!\n");                return true;            default:                printf("await_ready: status:%d, return false.\n", status);                return false;            }        }        // 在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立即resume协程        bool await_suspend(std::coroutine_handle<> awaiting){            printf("await_suspend invoked.\n");            tcp_client_.handle_ = awaiting;            return true;        }        // 在协程resume的时候会调用这个,这个的返回值会作为await的返回值        int await_resume(){            int ret = tcp_client_.status() == CONNECTED ? 0 : -1;            printf("awati_resume invoked, ret:%d\n", ret);            return ret;        }    };

理解了co_await之后,能够回头看一下之前的内容,后面屡次呈现的std::suspend_never和std::suspend_always就是两个预约义好的awaiter,也有那三个接口的定义,有趣味的同学能够看看对应的源代码。promise对象的initial_suspend、final_suspend、yield_value返回的都是awaiter,实际上零碎执行的是 co_await promise.initial_suspend() ,co_yield实际上执行的是 co_await promise.yield_value() 。如果有须要,也能够返回自定义的awaiter。

总结

能够看出C++20给出了一个非常灵活、有很弱小可定制性的协程机制,但短少根本的库反对,连写一个最简略的协程都须要开发者付出不少了解和学习的老本,目前的状态只能说是打了一个的地基,在C++23中,为协程提供库的反对是重要的指标之一,能够刮目相待。

参考资料:

1.协程 (C++20)

2.C++ 协程:理解运算符co_await

3.C++20行将到来的coroutine是否与Golang的goroutine媲美?

如果你是腾讯技术内容创作者,腾讯云开发者社区诚邀您退出【腾讯云原创分享打算】,支付礼品,助力职级降职。