【C++ 异步编程】C++ 20 的协程起手指南
C++20协程实践:从基础到异步任务处理 本文介绍了C++20协程的基本概念和实践应用。协程作为用户态轻量级线程,通过co_await和co_yield关键字实现控制权让出,解决了异步编程中的回调地狱问题。文章详细讲解了如何通过promise_type定义协程规则,并展示了Awaitable异步等待对象的实现。通过一个完整示例代码,演示了协程创建、异步任务处理和协程返回的全过程,帮助读者理解协程的
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
前言
从 【网络编程】NtyCo协程服务器的框架(轻量级的协程方案,人称 “小线程”) 到 【C++ 异步编程】没有 C++20 的协程特性之前,我们是怎样完成异步编程的。搞懂这个就能理解 “协程可以像同步编程那样完成异步编程”,当我 get 到协程是为了解决多个不同 ”异步耗时任务“ 的按流程序执行时产生的 ”回调地狱问题“ (对开发效率极为不利,尽管运行速度很快,不是性能的问题),我就知道我离学会 C++20 协程已经不远了。
本篇文章就先对 C++20 的协程做一个可以运行的小案例测试。我更大的愿望其实是,写一个是用协程完成百万并发的服务器程序。这是有点难的,但如果完成了,这说明我对做业务复杂的高性能服务器又往前进了一大步。
C++20 协程的介绍
C++20 的协程是广义上的 ”函数”,我们都知道普通的 C++ 函数只能够 “被线程调用”、“一字不落全部执行才返回”。而协程是可以主动让出协程的控制权,不必等协程的内容全部执行完再返回。
关于协程的八股文:协程是用户态轻量级线程,由程序自身控制调度(非抢占式),在单线程内实现多任务协作式并发。
有两个特别的关键字 co_await 和 co_yield,这意味着任何一个 C++20 的协程,其协程的控制权有两个让出对象,第一个是让出给调用它的主协程,即 co_yield,另一个让出对象是其他线程 co_await。这种设计其实很巧妙,我们的协程可以通过线程间通信来实现异步任务处理,同时也可以主动让出给主协程,供其调度差遣,有相当大的灵活性。

协程真的很像一个函数,他不过是多了 恢复、异步执行、让出协程 、协程返回 四个动作而已。线程里的主协程就是一整个程序的主线,而子协程是支线任务,就算分叉再多,弯弯绕绕再多,所有的协程执行完之后都得回到主协程里面。

书写一个协程
C++20 认为协程得要自己给自己定立规矩,协程要主动的区域其他协程进行协调协作,不可以做抢占线程的事情。所谓的规矩就是,异步等待、让出协程、协程返回、协程恢复的时候要做什么动作。只有订立了规矩,才有协作的基础,正如有了协议才有了网络通信,不然怎么叫“协程”呢?
而在 C++ 20 的协程库里面,有一种数据结构叫做 promise_type,他就是我们所要订立的 “规矩”,不仅要有规矩,还得要有异步调用链。定义一个写成所需要的大部分接口,都在下图显示。我们需要定义接口,来让这些关键字的使用有具体的语义。

还需要说明自己的协程运行到什么地方会让出协程、主协程什么时候会恢复协程、协程什么时候返回、主协程什么时候执行异步任务。这就是具体协程的实现了,这些实现都依赖于对协程接口的定义,完成了协程的接口定义,才可以书写协程,因为协程的挂起、让出、返回、恢复,这四个关键动作都是依赖于它们。
promise_type 是协程的规矩承诺
我的这份代码,来自于 B 站夏曹俊 C++ 基础实战课程的一段代码。我只是把它的代码都改了一下,把终端输出信息补充得更加详细,读者根据终端输出可以很快的理解代码的执行顺序。
自定义一个类型,只要其 “内嵌 / 组合” 一个 promise_type 类型接口,再组合一个 coroutine_handle 的句柄,用来获取promise_type 内部的数据 (我们恢复协程、迭代器输出都是要使用这个句柄的),那么我们就可以定义出协程的一个蓝本。
#include <iostream>
#include <coroutine>
#include <string>
#include <optional>
#include<thread>
#include <queue>
using namespace std;
template<typename T>
class Task
{
public:
struct promise_type
{
promise_type()
{
cout << "1 promise_type 构造函数,定立规矩" << endl;
}
~promise_type()
{
cout << "8 promise_type 析构函数,协程死亡,规矩作废" << endl;
}
auto get_return_object()
{
cout << "2 get_return_object 获取协程的句柄" << endl;
return Task{ coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend()
{
cout << "4 initial_suspend 协程创建之初,此处不挂起" << endl;
return {};
}
std::suspend_always final_suspend() noexcept
{
cout << "6 final_suspend 协程返回,此处挂起" << endl;
return {};
}
void unhandled_exception()
{
cout << "unhandled_exception 协程内部处理" << endl;
}
void return_void()
{
cout << "5 return_void 这个协程并不返回任何值" << endl;
}
std::suspend_always yield_value(T value)
{
cout << "yield_value(让出值、生成值): " << value << endl;
value_ = move(value);
return {};
}
// 协程不能直接让出
T value_;
};
coroutine_handle<promise_type> handle()
{
return handle_;
}
std::optional<T> Next()
{
if (!handle_ || handle_.done())
return std::nullopt;
handle_.resume();
if(handle_.done())return std::nullopt;
return handle_.promise().value_;
}
Task(coroutine_handle<promise_type> handle ):handle_(handle)
{
cout << "3 Task 协程构造函数,协程可以看作是一个任务函数" << endl;
}
~Task()
{
cout << "7 Task 析构函数,某种意义上是函数的析构" << endl;
if (handle_)
handle_.destroy();
}
private:
coroutine_handle<promise_type> handle_;
};
promise_type 与协程的关系图

Awaitable 异步等待对象
需要定义三个接口,名字不能写错,接口方法的名字是固定的。
// 异步等待时期的处理链
struct XEventAwait
{
bool await_ready()
{
cout << "await_ready:本次协程被设定成一定挂起\n";
return false;//挂起
}
void await_suspend(coroutine_handle<> handle)
{
cout << "XEvent await_suspend:协程投放异步任务,此处模拟异步任务,做出休眠" << endl;
this_thread::sleep_for(3s);
handle.resume();
}
string await_resume()
{
cout << "await_resume:异步任务已经完成,现在正在对异步任务返回值做数据处理,随后把值传递给协程" << endl;
return "testresume";
}
};
根据前两个类型定义一个协程
Task<string> TestCoroutine()
{
cout << "TestCoroutine 开始运行" << endl;
cout << "准备调用异步任务链" << endl;
string res = co_await XEventAwait{};
// 可以使用 std::suspend_always{}
cout << "after co_wait:异步等待 async-wait 完成,获取异步任务的处理结果 res =" << res << endl;
for (int i = 0; i < 4; i++)
{
string str = "test co_yield " + std::to_string(i + 1);
co_yield str;
}
cout << "协程即将返回" << endl;
co_return;
cout << "TestCoroutine 结束 不会被调用,因为协程不会往 co_return 下方运行" << endl;
}
综合前面的代码,我结合接口和对象,来描述主协程、协程、异步等待、异步任务之间的关系图,搞清楚这些接口的调用关系,我们就会很好的完成协程的定义。

再对比开头的这张图,我们就能清晰的 get 到 C++20 协程的架构使用逻辑了。

测试协程
int main()
{
{
auto task = TestCoroutine();
cout << "这是协程第一次让出给主协程" << endl;
cout << "接下来,协程进入生成器模式:resume 接口守在生成函数里面" << endl;
// task.handle().resume();
while (auto line = task.Next())
{
cout << "主协程获取来自生成器的数据: " << *line << endl;
}
cout << "生成器任务结束" << endl;
cout << "即将离开作用域,即使协程最后是被挂起且没有被恢复执行,也会自动析构对象" << endl;
}
}
运行之后,我们大概就清楚了它的语法,各个语句之间的调用顺序。大家比对终端输出和代码的文本输出流语句就会明白是 C++20 协程的接口怎么一回事了。
qiming@k8s-master1:~/share/mycpp_work/coroutine-network$ g++ -std=c++20 -o test coroutine_test.cpp -g
qiming@k8s-master1:~/share/mycpp_work/coroutine-network$ ./test
1 promise_type 构造函数,定立规矩
2 get_return_object 获取协程的句柄
3 Task 协程构造函数,协程可以看作是一个任务函数
4 initial_suspend 协程创建之初,此处不挂起
TestCoroutine 开始运行
准备调用异步任务链
await_ready:本次协程被设定成一定挂起
XEvent await_suspend:协程投放异步任务,此处模拟异步任务,做出休眠
await_resume:异步任务已经完成,现在正在对异步任务返回值做数据处理,随后把值传递给协程
after co_wait:异步等待 async-wait 完成,获取异步任务的处理结果 res =testresume
yield_value(让出值、生成值): test co_yield 1
这是协程第一次让出给主协程
接下来,协程进入生成器模式:resume 接口守在生成函数里面
yield_value(让出值、生成值): test co_yield 2
主协程获取来自生成器的数据: test co_yield 2
yield_value(让出值、生成值): test co_yield 3
主协程获取来自生成器的数据: test co_yield 3
yield_value(让出值、生成值): test co_yield 4
主协程获取来自生成器的数据: test co_yield 4
协程即将返回
5 return_void 这个协程并不返回任何值
6 final_suspend 协程返回,此处挂起
生成器任务结束
即将离开作用域,即使协程最后是被挂起且没有被恢复执行,也会自动析构对象
7 Task 析构函数,某种意义上是函数的析构
8 promise_type 析构函数,协程死亡,规矩作废
总结
C++20 协程的架构就说到这里了。我们还是对比一下它跟 NtyCo 开源框架的区别。
NtyCo 协程框架的流水线式的逻辑图如下,他是跟 C++20 的协程架构不一样的。C++20 的协程从未离开过本线程!他只是给异步线程派发任务。

更多推荐



所有评论(0)