【并发编程二十一】c++20协程(co_yield、co_return、co_await )
- 一、协程分类
- 1、控制机制划分
- 2、有栈(stackfull)/无栈(stackless)划分
- 二、c++20协程
- 三、co_yield
- 1、demo
- 2、相关知识点介绍
- 四、co_return
- 五、co_await
一、协程分类
上一篇我们讲解了协程实现的多种方式,但是我们没有讲解协程的分类。在此我们讲解下。
1、控制机制划分
- 非对称协程(asymmetric coroutines):是跟一个特定的调用者绑定的,协程让出CPU时,只能让回给原调用者。
- 对称协程(symmetric coroutines):则不同,被调协程启动之后就跟之前运行的协程没有任何关系了。
2、有栈(stackfull)/无栈(stackless)划分
有栈协程:(类似线程)每一个协程都会有自己的调用栈。(一般的协程使用栈内存来存储数据)
无栈协程:但是无栈协程不具备数据栈。
无栈协程其实现原理是将执行的方法编译为一个状态机,实现的时候不需要在临时栈和系统栈直接拷贝现场。因此无栈协程的效率和占用的资源更少。当然,有栈协程的代码会更加的简单易读。
(go语言是有栈协程——对go语言不熟悉,未验证)
(python是无栈协程——未验证)
二、c++20协程
协程就是一个可以挂起执行,稍后再恢复执行的函数。只要一个函数包含 co_await、co_yield 或 co_return 关键字,则它就是协程。
C++20 中的协程是无栈式的(stackless),协程挂起时会返回到调用者或恢复者,且恢复执行所需的数据会分开存储,而不放在栈上。
另外再说一点,我看有的文章说c++20协程是非对称协程,也就是说必须返回调用者。但是我们写代码时发现,其实也可以调用其他的,所以对网上的这些说法我持保留意见。
三、co_yield
1、demo
先写一个不能运行的c++20协程协程
MyCoroutine<int> task()
{
int a = 0, b = 1;
while (a <= 10)
{
co_yield a;
a++;
}
};
int main()
{
MyCoroutine<int> fun = task();
while (fun.MoveNext())
{
cout << fun.GetValue()<<endl;
//getchar();
}
}
之所以我们说不能运行的协程是因为,因为我们上面说了,含有上面三个关键字中的就是协程,但是协程函数返回值的类型,必须是一个自定义类型,并且这个自定义类型需要按照一定的格式来定义。比如上文中的代码MyCoroutine就是我们按照要求自定义的类型。
- 下面我们写一个完整的挂起。并展示下如何自定义协程的返回类型。
#include <iostream>
#include<coroutine>
using namespace std;
template<typename T>
class MyCoroutine
{
public:
// 协程开始时,在协程的状态对象分配内存后,调用promise_type的构造函数
struct promise_type {
T value;
// 为协程的状态对象分配内存失败时
static auto get_return_object_on_allocation_failure() { return MyCoroutine{ nullptr }; }
// 构造成功后开始执行
auto get_return_object() { return MyCoroutine{ handle::from_promise(*this) }; }
// 在以上函数后执行
auto initial_suspend() { return std::suspend_always{}; }
// 协程结束前执行
auto final_suspend() noexcept { return std::suspend_always{}; }
// 出现未经处理的异常时执行
void unhandled_exception() { return std::terminate();}
// co_return 时执行,return_void跟return_value二选一
void return_void(){}
//int return_value(int result) { this.result = reslut; }
//co_yield时执行
auto yield_value(T value ) {this->value=value; return std::suspend_always{}; }
};
using handle = std::coroutine_handle<promise_type>;
private:
handle hCoroutine;
MyCoroutine(handle handle) :hCoroutine(handle) {}
public:
//int result;
MyCoroutine(MyCoroutine&& other)noexcept :hCoroutine(other.hCoroutine) { other.hCoroutine = nullptr; }
~MyCoroutine() { if (hCoroutine) hCoroutine.destroy(); }
bool MoveNext() const { return hCoroutine && (hCoroutine.resume(), !hCoroutine.done()); }
T GetValue() const { return hCoroutine.promise().value; }
};
MyCoroutine<int> task()
{
int a = 0, b = 1;
while (a <= 10)
{
co_yield a;
a++;
}
};
int main()
{
MyCoroutine<int> fun = task();
while (fun.MoveNext())
{
cout << fun.GetValue()<<endl;
//getchar();
}
}
输出
2、相关知识点介绍
对比代码中的备注,我们知道
-
协程开始时,在协程的状态对象分配内存后,调用promise_type的构造函数
struct promise_type
-
为协程的状态对象分配内存失败时
static auto get_return_object_on_allocation_failure() { return MyCoroutine{ nullptr }; }
- 构造成功后开始执行
auto get_return_object() { return MyCoroutine{ handle::from_promise(*this) }; }
- 在以上函数后执行
auto initial_suspend() { return std::suspend_always{}; }
- 协程结束前执行
auto final_suspend() noexcept { return std::suspend_always{}; }
- 出现未经处理的异常时执行
void unhandled_exception() { return std::terminate();}
- co_return 时执行,return_void跟return_value二选一
void return_void(){}
//int return_value(int result) { this.result = reslut; }
- co_yield时执行
auto yield_value(T value ) {this->value=value; return std::suspend_always{}; }
- coroutine_handle也暴露出多个接口,用于控制协程的行为、获取协程的状态,与promise_type不同的是,promise_type里的接口需要我们填写实现,promise_type里的接口是给编译器调用的。coroutine_handle的接口不需要我们填写实现,我们可以直接调用。
coroutine_handle接口 | 作用 |
---|---|
from_promise() | 从promise对象创建一个coroutine_handle |
done() | 检查协程是否运行完毕 |
operator bool | 检查当前句柄是否是一个coroutie |
operator() | 恢复协程的执行 |
resume | 恢复协程的执行(同上) |
destroy | 销毁协程 |
promise | 获取协程的promise对象 |
address | 返回coroutine_handle的指针 |
from_address | 从指针导入一个coroutine_handle |
四、co_return
#include <coroutine>
#include <iostream>
#include <memory>
template<typename T>
struct MyCoroutine {
struct promise_type;
std::coroutine_handle<promise_type> m_handle;
MyCoroutine(std::coroutine_handle<promise_type> handle) // (3)
: m_handle(handle) {
}
~MyCoroutine() {
m_handle.destroy(); // (11)
}
T get() { // (10)
m_handle.resume();
return m_handle.promise().value;
}
struct promise_type {
T value = {};
promise_type() { // (4)
}
~promise_type() { }
auto get_return_object() { // (7)
return MyCoroutine<T>{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
void return_value(T v) { // (8)
value = v;
}
auto initial_suspend() { // (5)
return std::suspend_always{};
}
auto final_suspend() noexcept { // (6)
return std::suspend_always{};
}
void unhandled_exception() {
std::exit(1);
}
};
};
MyCoroutine<int> task() {
co_return 2023; // (9)
}
int main() {
auto fut = task();
std::cout << "fut.get(): " << fut.get() << '\n'; // (2)
}
输出如下
五、co_await
- 以下demo是利用chatGBT写的代码基础上更改而来,(因为chatGBT写的代码编译失败)
我们先参考co_yield和co_return,写一个await的例子。(其实这里并非co_await的真正用法,还有其他的用法)
#include <iostream>
#include<coroutine>
using namespace std;
struct MyCoroutine {
struct promise_type;
std::coroutine_handle<promise_type> m_handle;
MyCoroutine(std::coroutine_handle<promise_type> handle) // (3)
: m_handle(handle) {
}
~MyCoroutine() {
m_handle.destroy(); // (11)
}
struct promise_type {
auto get_return_object() { return MyCoroutine{ std::coroutine_handle<promise_type>::from_promise(*this) }; }
auto initial_suspend() { return std::suspend_never{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
bool resume() {
if (!m_handle.done())
m_handle.resume();
return !m_handle.done();
}
};
MyCoroutine func() {
std::cout << "Starting coroutine" << std::endl;
co_await std::suspend_always{};
std::cout << "Resuming coroutine" << std::endl;
}
int main() {
auto gen = func();
std::cout << "Yielding coroutine" << std::endl;
gen.resume();
std::cout << "Finished" << std::endl;
return 0;
}
输出
至于真正的等待器的三个关键字的用法我就不写了,感兴趣的同时可以看bilibili的这个讲解。【C++20 协程(2/2)】可等待体和等待器,or稍微啰嗦的这个博客,因为太啰嗦我没仔细看细节,但是里面讲到了等待器的部分知识。所以还是推荐上面哔哩哔哩的视频up主C++20 协程(2):理解co_await运算符
后记:
【并发编程】系列到此就结束了,从2022年10月底写下第一篇【并发编程一】进程、线程、协程、芊程,到现在一步步梳理到c++20协程,最大的收获就是,之前零散的、不清晰的知识点,逐步变成了清晰的知识面。对计算机组成原理、操作系统、网络、c++相关等都有了一定的了解,收获和成长了很多,知识体系也逐步的建立起来了。
参考
1、C++20四大之二:coroutines特性详解
2、C++20 协程(一):
3、C++20协程