C++20协程学习

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

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

C++20协程学习

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的地方,持续开始履行。

C++20协程学习

Promise

number_generator的回来类型是coro_ret,而协程本身的代码中并没有经过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 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()假如协程内的代码抛出了异常,那么这个接口会被调用。

C++20协程学习

协程相关方针

能够看出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协程学习

C++20协程学习

暂时总结

要在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方法。

en.cppreference.com/w/cpp/langu…

C++20协程学习

用代码表达,是这样:

 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媲美?

C++20协程学习

假如你是腾讯技能内容创作者,腾讯云开发者社区诚邀您参加【腾讯云原创共享方案】,收取礼品,助力职级提升。

阅读原文