c++20协程入门分析

时间:2024-10-27 15:14:04

c++20协程

c++20标准出现了协程,但是以我从零开始的学习者来描述的话,它真的很难,虽然说已经支持了协程,但是需要自己写的代码非常多,而且非常复杂,协程之间的来回跳转,逻辑很复杂,这篇文章我来简单介绍一下协程,让大家更容易的去学习。,主要讲下几个协程语句和所需要的数据结构,关键字有co_awaitco_yeildco_return,返回值结构体和可等待体,ok,我们开始分析,我尽量让大家容易入门。

这里先来一个简单的例子,对简单的例子已经50多行了,可想而知,协程还是挺复杂的。我们就从这里例子让你了解c++协程。

#include <coroutine>
#include <iostream>

using namespace std;

struct coRet {
  struct promise_type {
    int mainValue = -1;
    int yieldValue = -1;
    int returnValue = -1;
    int awaitValue = -1;
    suspend_always initial_suspend() { return {}; }
    suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    coRet get_return_object() {
      return {coroutine_handle<promise_type>::from_promise(*this)};
    }
    suspend_always yield_value(int r) {
      yieldValue = r;
      return {};
    }
    void return_value(int r) { returnValue = r; }
  };

  coroutine_handle<promise_type> _h;
};
struct awaiter {
  bool await_ready() { return false; }
  void await_suspend(coroutine_handle<coRet::promise_type> h) {
    value = h.promise().mainValue;
    h.promise().awaitValue = 88;
  }
  int await_resume() { return value; }
  int value = -1;
};

coRet coroutineFunc() {
  cout << "coroutine begin" << endl;
  auto awaitRes = co_await awaiter{};
  cout << "await return value " << awaitRes << endl;
  co_yield(10);
  co_return 11;
}

int main() {
  auto cores = coroutineFunc();
  cout << "main begin" << endl;
  cores._h.promise().mainValue = 100;
  cores._h.resume();
  cout << "resume1 : " << cores._h.promise().awaitValue << endl;
  cores._h.resume();
  cout << "resume2 : " << cores._h.promise().yieldValue << endl;
  cores._h.resume();
  cout << "resume3 : " << cores._h.promise().returnValue << endl;
  return 0;
}

返回值结构体

只要函数中包括co_awaitco_yeildco_return这些关键字,那么这个函数就不是普通的函数,就是一个协程,协程的返回值必须是按照规定定义出来的结构体,因为这个结构体会包含协程的句柄,来控制协程的调度的,因此我们先来分析这个返回值结构体,下面是我把返回值结构体的代码抽出来了。

struct coRet {
  struct promise_type {
    int mainValue = -1;
    int yieldValue = -1;
    int returnValue = -1;
    int awaitValue = -1;
    suspend_always initial_suspend() { return {}; }
    suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    coRet get_return_object() {
      return {coroutine_handle<promise_type>::from_promise(*this)};
    }
    suspend_always yield_value(int r) {
      yieldValue = r;
      return {};
    }
    void return_value(int r) { returnValue = r; }
  };

  coroutine_handle<promise_type> _h;
};

我们可以看出核心的数据结构是promise_type,这个不是胡乱定义的,是所有协程的返回值结构体内部必须有一个这样的定义,promise_type是来掌控协程的主要逻辑的,我们来分析一下promise_type结构体,这个结构体里面需要实现一些接口,这里我们针对每一个接口进行解释,了解它的用处。

  • initial_suspend()是用来控制协程开始时是否直接返回,这个函数如果返回suspend_always结构体的话,就是协程起始之处就返回,就是执行用户代码之前直接挂起,如果返回suspend_never结构体就直接执行用户代码,遇到co_awaitco_yieldco_return时再挂起。
  • final_suspend()是在函数结束时是否挂起的,也就是在用户代码全部执行完之后,suspend_alwayssuspend_never的作用和initial_suspend一样
  • get_return_object()这个是根据promise来创建返回值,也就是本协程的返回值,这个返回值会包括本协程句柄,协程的返回值就是这个函数的返回值
  • unhandled_exception()这个是协程异常的时候的处理逻辑
  • yield_value()是协程调用co_yield时调用的函数,展开之后是co_await yield_value(),可以有参数,比我我们上面的例子co_yield(10) -> co_await yield_value(10)
  • return_value()是协程调用co_return时调用的函数,当有返回值的时候,返回值作为return_value()的参数,我们的例子时一个int整型,没有返回值的时候函数名就编程了void_value()

上面几个函数就是返回值类型必须需要定义的,否则这个返回值就不能作为协程的返回值使用,然后协程返回值类型一般都会包含一个协程的句柄,方便后续对协程的调度,返回值类型了解这么多就可以了,当然初学者可能会蒙蔽,不知道这样做的原因,这些函数到底咋发挥作用,别急,正常的现象,因为我刚开始学的时候也懵逼,后面基础知识都学完了,串起来就容易理解了,后面给你逻辑性的展开协程的代码,你就知道全貌了,后面我们再分析可等待体。

可等待体

可等待体就是关键词co_await后面跟的变量,co_await后面的变量也是必须实现几个接口才可以,要么就不合法,下面是我把可等待体的代码抽出来。

struct awaiter {
  bool await_ready() { return false; }
  void await_suspend(coroutine_handle<coRet::promise_type> h) {
    value = h.promise().mainValue;
    h.promise().awaitValue = 88;
  }
  int await_resume() { return value; }
  int value = -1;
};

和上面的promise_type结构体一样,我们分析这几个接口。

  • await_ready() 这个函数就是调用co_await awaiter后,先进行这个函数的调用判断,如果返回是true的话,协程不会挂起,直接返回向下执行,co_await返回其实就是await_resume()的返回。
  • await_suspend() 这个是调用co_await awaiter后,并且await_ready()返回false后才会执行这个函数,这个函数会将协程挂起了,函数参数就是协程的句柄,返回值有三类,如果是void的话,那么就直接将协程挂起,执行权回到调用协程的函数或者协程,如果是返回一个协程句柄,那么本协程将挂起,然后执行权调度到返回值句柄的协程中,如果是bool类型的话,返回false的话,就直接恢复协程,不挂起,如果返回true的话,和void的逻辑一样,返回给调用协程的协程或者函数。
  • await_resume()就如上面描述await_ready()中写到的一样,就是co_await返回时调用的函数。

协程关键字

我们分析完上面关于协程的两大数据结构后,我们来分析下协程的几个关键字分别是co_awaitco_yeildco_return,这些关键字的逻辑和我们上面分析的协程返回值和可等待体息息相关,把我们挨个分析下。

  • co_await 可以从这个关键词中看出来,就是等待挂起的意思,但是是否真的挂起是我们程序员来决定的,也就是我们上面提到的可等待体,是否挂起,挂起后切换到哪个协程都是我们定义的可等待体里面的接口实现决定的,比如我们的例子,就是直接将协程挂起,执行权交给主函数。
  • co_yeild 我们还是可以从这个关键字看出有让出的意思哈,但是是否真的让出还是由我们程序员来决定,可以通过上述我们对协程返回值结构体的分析,和这个关键词相关的处理逻辑是协程返回值结构体中的yield_value()函数,因为co_yield会展开为co_await yield_value()yield_value()的返回值就是一个可等待体,那么这个分析的逻辑就是co_await awaiter一样了,也就是我们对co_await的分析,这个可等待体我们可以自己设计,或者使用标准库里的数据结构,因此可设计空间很大,我们的例子是使用了suspend_always()也就是直接让出了,但是yield_value()我们也加了一些额外的逻辑。
  • co_return 可以看出,这个是协程返回,也就是协程的结束了(用户代码的结尾,因为协程还会加一些用户看不到的代码在协程开始和末尾),他的控制逻辑就是协程返回值结构体的return_value()void_value(),如果co_return后有值,那么就调用return_value(),没值的话就调用void_value(),相对来说简单

代码逻辑性扩充和执行权转移分析

上面我们对协程的三大块进行了分析,相信大家还是懵逼的,因为确实不好串联在一起,而且自定义程度较大,也就会相对复杂,不能傻瓜式使用,因此我们根据我们的分析,来对协程的代码进行逻辑性的扩充(不一定对嗷,只是进行逻辑性扩充,真正的代码不一定),然后分析下执行权的转移路线。

代码逻辑性扩充

coRet coroutineFunc() {
  // 协程前扩充的代码
  coroutine_handle<coRet::promise_type> h;
  coRet::promise_type promise = h.promise();
  coRet ret = promise.get_return_object();
  co_await promise.initial_suspend();
  //

  cout << "coroutine begin" << endl;
  // 展开的代码
  awaiter.await_suspend(h);
  // 
  // auto awaitRes = co_await awaiter{}; //这一句会变成awaiter.await_suspend(h)去挂起,然后恢复的时候调用awaitRes = awaiter.await_resume()
  // 展开的代码
  auto awaitRes = awaiter.await_resume();
  //
  cout << "await return value " << awaitRes << endl;
  // 展开的代码
  co_await promise.yield_value(10);
  // 
  // co_yield(10); //这一句会变成co_await promise.yield_value(10); 然后根据await的逻辑进行分析就可以
  // 展开的代码
  promise.return_value(11);
  // 
  // co_return 11; //这一句会变成promise.return_value(11); 然后协程的用户代码结束了

  // 协程最后扩充的代码
  co_await promise.final_suspend();
  //
}

上面的代码,就是我们逻辑性扩充的协程代码,协程用户代码前后会有些添加,关键字会展开调用一些函数,通过上面的扩展,我们大概能了解到上面我们对协程三大主要部分分析的作用了,改变完后全变co_await

执行权转移分析

这个我们直接来张图分析吧,代码还不好画出执行权转移的箭头,看着扩充的代码和执行权转移图,再配上上面我们分析的协程三大件,基本上可以对协程有个全貌的认识。

在这里插入图片描述

根据对上面图的分析,我们可以得到程序执行的输出理应是

main begin
coroutine begin
resume1 : 88
await return value 100
resume2 : 10
resume3 : 11

经过我对上述代码的编译和执行得到的结果是一直的,因此我们是没有问题的,编译的时候记得使用20的标准,要么编译失败,下面是验证的截图。

在这里插入图片描述