C++协程

python程序异步协程程序、asyncio中使用python实例说明了协程可以起到基本知识,可以先看一下。

1.C++协程

参考:1. C++ 协程概览

协程:就是一个支持挂起和恢复的函数。在 C++ 当中,一个函数的返回值类型如果是符合协程的规则的类型,那么这个函数就是一个协程。

【注】如果输入协程中实参是引用、指针类型,那么协程挂起时,保存的是引用或指针本身,而不是其指向的对象,这时候需要开发者自行保证协程在挂起后续恢复执行时参数引用或者指针指向的对象仍然存活。

1.1 co_await与awaiter

C++ 通过 co_await 表达式来处理协程的挂起,如下 :

co_await std::suspend_always{}; // 总是挂起
co_await std::suspend_never{}; // 总是不挂起 【问题】不挂起,定义来干嘛?co_await不是用来挂起的吗?

std::suspend_always{}为等待体(awaiter),我们还可以自定义awaiter。自定义awaiter需要实现三个函数,这三个函数在挂起和恢复时分别调用。三个函数如下:
1.bool await_ready();返回 bool 类型,如果返回 true,则表示已经就绪,无需挂起;否则表示需要挂起。

2.??? await_suspend(std::coroutine_handle<> coroutine_handle);await_ready 返回 false 时,协程就挂起了。这时候协程的局部变量和挂起点都会被存入协程的状态当中,然后await_suspend 被调用到。参数 coroutine_handle 用来表示当前协程,我们可以在稍后合适的时机通过调用 resume 来恢复执行当前协程:coroutine_handle.resume();
注意到 await_suspend 函数的返回值类型我们没有明确给出,因为它有以下几种选项:

  • 返回 void 类型或者返回 true,表示当前协程挂起之后将执行权还给当初调用或者恢复当前协程的函数。
  • 返回 false,则恢复执行当前协程。注意此时不同于 await_ready 返回 true 的情形,此时协程已经挂起,await_suspend 返回 false 相当于挂起又立即恢复。
  • 返回其他协程的 coroutine_handle 对象,这时候返回的 coroutine_handle 对应的协程被恢复执行。
  • 抛出异常,此时当前协程恢复执行,并在当前协程当中抛出异常。

可见,await_suspend 支持的情况非常多,也相对复杂。实际上这也是 C++ 协程当中最为核心的函数之一了。

3.??? await_resume();协程恢复执行之后,等待体的 await_resume 函数被调用。同样地,await_resume 的返回值类型也是不限定的,返回值将作为 co_await 表达式的返回值。

了解了以上内容以后,我们可以自己定义一个非常简单的等待体:

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

struct Result {
    struct promise_type {
        Result get_return_object() { std::cout << "get_return_object" << std::endl; return {};}
        std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
        std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
        void return_void() { std::cout << "return_void" << std::endl; }
        void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
    };
};

struct Awaiter {
  int value;

  bool await_ready() {
    // 协程挂起
    return false;
  }

  void await_suspend(std::coroutine_handle<> coroutine_handle) {
    // 切换线程
    std::async([=](){
      using namespace std::chrono_literals;
      // sleep 1s
      std::this_thread::sleep_for(1s); 
      // 恢复协程
      coroutine_handle.resume();
    });
  }

  int await_resume() {
    // value 将作为 co_await 表达式的值
    return value;
  }
};

Result Coroutine() {
  std::cout << 1 << std::endl;
  std::cout << co_await Awaiter{.value = 1000} << std::endl; //不知道value前“.”的含义,不过没有就会报错。 
  std::cout << 2 << std::endl; // 1 秒之后再执行
};


int main() {
    std::cout << "start main" << std::endl;
    Result x =  Coroutine();
    std::cout << "end main" << std::endl;
}

程序运行结果如下:

$  g++-10 test.cpp -std=c++2a -fcoroutines  -pthread  -o a.out 
$ ./a.out 
start main
get_return_object
initial_suspend
1
1000
2
return_void
final_suspend
end main

其中 "1000" 在 "1" 输出 1 秒之后输出。Result的定义规则下面会讲到。

1.2 协程的返回值类型(Result的定义规则)和实现一个序列生成器

在 C++ 当中,一个函数的返回值类型如果是符合协程的规则的类型,那么这个函数就是一个协程。接下来实现一个序列生成器来说明协程的返回值类型。

序列生成器:resume一次协程,返回一个值,然后暂停协程,等待下次被resume。代码如下:

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

struct Generator {
  struct promise_type {
    int value;
    std::suspend_always initial_suspend() { return {}; };
    std::suspend_never final_suspend() noexcept { return {}; } // 执行结束后不需要挂起。noexcept代表协程结束了,不能抛异常了。
    void unhandled_exception() { } // 为了简单,我们认为序列生成器当中不会抛出异常,这里不做任何处理
    Generator get_return_object() {  // 构造协程的返回值类型
      return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    void return_void() { }     // 没有返回值
   
    std::suspend_always await_transform(int value) {
      this->value = value;
      return {};
    }
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();

  // 外部调用者或者恢复者可以通过读取 value
    return handle.promise().value;
  }
};


Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto gen = sequence();
  for (int i = 0; i < 5; ++i) {
    std::cout << gen.next() << std::endl;
  }
}

协程sequence()的调用流程:

1.执行get_return_object返回协程的返回值

  • get_return_object:不同于一般的函数,协程的返回值并不是在返回之前才创建,而是在协程的状态创建出来之后马上就创建的。也就是说,协程的状态被创建出来之后,会立即构造 promise_type 对象,进而调用 get_return_object 来创建返回值对象。get_return_object返回的就是协程需要返回的对象。

  • promise_type 类型的构造函数参数列表如果与协程的参数列表一致,那么构造 promise_type 时就会调用这个构造函数。否则,就通过默认无参构造函数来构造 promise_type。

  • Generator{std::coroutine_handle<promise_type>::from_promise(*this)中的this代表promise_type对象,此代码代表使用from_promise函数从promise_type对象中获取coroutine_handle,coroutine_handle是用于操作和控制协程。
    【注】std::coroutine_handle<promise_type>::from_promise(*this)会传递给Generator的数据成员handle,即使Generator没有写对应的构造函数。

  • 当Generator中有了handle,就可以使用handle在Generator类中对协程进行操作和控制。就比如当Generator对象调用next()函数时,使用handle.resume();恢复协程。再比如代码中promise_type类型中定义了value变量,通过handle就可以访问到value变量。

2.执行initial_suspend:本代码中initial_suspend返回suspend_always代表挂起,可以调用resume将挂起的协程恢复。

3.执行协程体sequence中的内容:由于initial_suspend返回的是suspend_always,所以协程体不会马上运行,而是等待resume被调用时,才会恢复协程运行。在协程体运行的过程中,可以使用co_yield和co_await将重新协程挂起,可以co_return结束协程。

4.co_return与co_yield:
co_return;会调用return_void()函数。co_return 1000;会调用return_value(int value)函数,return_value函数传入的参数就是1000。
co_return会调用相应函数并终止协程。
co_yield;会调用yield_void()函数。co_yield 1000;会调用yield_value(int value)函数,yield_value函数传入的参数就是1000。co_yield将暂停执行。

5.执行final_suspend函数做协程结束后的一些处理。final_suspend 来方便开发者自行处理其他资源的销毁逻辑。final_suspend 也可以返回一个等待体使得当前协程挂起,但之后当前协程应当通过 coroutine_handle 的 destroy 函数来直接销毁,而不是 resume

6.unhandled_exception用于异常处理。

其他解释:

co_await i++;:观察代码中的co_await i++;,发现co_await 后面的对象不是等待体,这类情况需要定义其他的函数和运算符来转换成等待体。实际上,对于co_await <expr>表达式当中 expr 的处理,C++ 有一套完善的流程:

  • 如果 promise_type 当中定义了 await_transform 函数,那么先通过 promise.await_transform(expr) 来对 expr 做一次转换,得到的对象称为 awaitable;否则 awaitable 就是 expr 本身。

  • 接下来使用 awaitable 对象来获取等待体(awaiter)。如果 awaitable 对象有operator co_await 运算符重载,那么等待体就是 operator co_await(awaitable),否则等待体就是 awaitable 对象本身。
    当然如果没有await_transform函数,但<expr>operator co_await运算符重载,那么那么等待体就是 operator co_await(<expr>)

听上去,我们要么给 promise_type 实现一个 await_tranform(int) 函数,要么就为整型实现一个 operator co_await 的运算符重载,二者选一个就可以了。本代码调用await_transform函数将i的值传递给协程内部,因为在 C++ 当中我们是无法给基本类型定义运算符重载的。

await_transform()的返回值是{},这是什么意思:在C++中,花括号{}可以用于初始化对象或者表示一个默认构造的对象,如

int x{};  // x被初始化为0
std::string str{};  // str被初始化为空字符串

实例

参考C++那些事之C++20协程

C++20引入了协程(Coroutines)的支持,这是一种可以让函数暂停并在稍后恢复执行的新特性。协程可以用于简化异步代码、生成器等等。

以下是一个简单的示例:

#include <iostream>
#include <coroutine>
struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
        std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
        std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
        void return_void() { std::cout << "return_void" << std::endl; }
        void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
    };
};
HelloTask myCoroutine() {
    std::cout << "create coroutine" << std::endl;
    co_return; // make it a coroutine
}
int main() {
    std::cout << "start main" << std::endl;
    HelloTask x = myCoroutine();
    std::cout << "end main" << std::endl;
}

输出:

start main
get_return_object
initial_suspend
create coroutine
return_void
final_suspend
end main

流程如下:

  • 在堆上分配coutine对象,构造promise,调用promise.get_return_object返回当前对象,这里这么做的目的是把结果保存到了局部变量里面。
  • 调用promise.initial_suspend,这里会触发co_await,输出create coroutine。
  • 执行协程体后,调用co_return,co_return调用promise.return_void
  • 最后调用promise.final_suspend
  • unhandled_exception:用于捕获异常。

co_yield使用实例

#include <iostream>
#include <coroutine>

struct generator {
    struct promise_type {
        int current_value;
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend()  noexcept  { return {}; }
        generator get_return_object() { return generator{this}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    generator(promise_type* p)
        : coroutine_handle{std::coroutine_handle<promise_type>::from_promise(*p)} {}

    ~generator() { coroutine_handle.destroy(); }

    std::coroutine_handle<promise_type> coroutine_handle; // 协程句柄,用于管理暂停或执行的协程以及Promise对象

    int next() {
        coroutine_handle.resume();
        return coroutine_handle.promise().current_value;
    }
};

generator fib(int n) {
    int a = 0, b = 1;
    while (n--) {
        co_yield b;
        auto t = a;
        a = b;
        b += t;
    }
}

int main() {
    auto gen = fib(10);
    for (int i = 0; i < 10; i++) {
        std::cout << gen.next() << '\n';
    }
}

这个程序定义了一个名为generator的类,它是协程的一种封装。generator具有一个promise_type的嵌套类型,promise_type是生成器的核心部分,它负责协程的控制流程。promise_type的实例被存储在协程对象中,并在协程启动时构建。

  • promise_type的成员函数yield_value()负责将生成器的下一个值(即int value)返回给调用者,同时暂停协程的执行。

  • initial_suspend()和final_suspend()方法负责协程的起始和结束暂停。

  • get_return_object()方法返回一个生成器实例,并将生成器的状态与promise_type实例关联。

  • generator的构造函数从promise_type的指针构造一个协程句柄。generator的析构函数销毁协程句柄。

  • 在fib()函数中,我们定义了一个生成器来计算斐波那契数列的前n个数。co_yield关键字用于暂停协程的执行并返回值。每次生成器被恢复时,它会继续执行之前暂停的位置。

  • 在main()函数中,我们创建了一个生成器实例,并通过调用next()方法逐个获取斐波那契数列的值。输出结果如下:

1
1
2
3
5
8
13
21
34
55

最开始,gen.next()先将initial_suspend()挂起的协程放下来,然后co_yield会被调用,即yield_value()会被调用,执行完yield_value()以后,协程又会被挂起,然后执行return coroutine_handle.promise().current_value;

co_await

不需要向类中传入参数,那么直接使用co_await,不用使用co_yield,如下:

#include <iostream>
#include <coroutine>
#include <thread>

struct generator {
    struct promise_type {
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend()  noexcept  { return {}; }
        generator get_return_object() { return generator{this}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    generator(promise_type* p)
        : coroutine_handle{std::coroutine_handle<promise_type>::from_promise(*p)} {}

    ~generator() { coroutine_handle.destroy(); }

    std::coroutine_handle<promise_type> coroutine_handle; // 协程句柄,用于管理暂停或执行的协程以及Promise对象

    void resume() {
        coroutine_handle.resume();

    }
};

generator fib() {
    while (true) {
        co_await std::suspend_always{};
        std::cout << "执行某些任务" << std::endl;
    }
}

int main() {
    auto gen = fib();
    while(true) { // 满足某个条件后恢复协程
        std::chrono::milliseconds dura(5000);
        std::this_thread::sleep_for(dura);// 模拟某些任务
        gen.resume() ;
    }
}
posted @ 2023-03-10 22:11  好人~  阅读(1381)  评论(0编辑  收藏  举报