【C++ 异步编程】C++ 20 的协程起手指南

【C++ 异步编程】C++ 20 的协程起手指南

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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_awaitco_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>usingnamespace std;template<typenameT>classTask{public:structpromise_type{promise_type(){ cout <<"1 promise_type 构造函数,定立规矩"<< endl;}~promise_type(){ cout <<"8 promise_type 析构函数,协程死亡,规矩作废"<< endl;}autoget_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{};}voidunhandled_exception(){ cout <<"unhandled_exception 协程内部处理"<< endl;}voidreturn_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 异步等待对象

需要定义三个接口,名字不能写错,接口方法的名字是固定的。

// 异步等待时期的处理链structXEventAwait{boolawait_ready(){ cout <<"await_ready:本次协程被设定成一定挂起\n";returnfalse;//挂起}voidawait_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 协程的架构使用逻辑了。


在这里插入图片描述

测试协程

intmain(){{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 的协程从未离开过本线程!他只是给异步线程派发任务。


在这里插入图片描述

Read more

c++好用的刷题网址(学习c++的必看系列)

c++好用的刷题网址(学习c++的必看系列)

作为学习有很多方向,我按照分类给出。 按照刷题专用,初级,高级,API参考等进行分类,建议先收藏防止找不到 一、刷题专用 以下内容供刷题使用。 1、 LeetCode - C++ Just a moment... leetcode.com/problemset/?topicSlugs=cpp 简介: 提供大量C++算法和数据结构题目,支持在线评测。 推荐理由: 面试准备必备,提升C++编码能力。 2、HackerRank - C++ https://www.hackerrank.com/domains/cpp www.hackerrank.com/domains/cpp 简介: 提供C++编程挑战和竞赛,涵盖基础到高级题目。 推荐理由:

By Ne0inhk
华为OD机试双机位C卷:最佳信号覆盖问题 (C/C++/Py/Java/Js/Go)

华为OD机试双机位C卷:最佳信号覆盖问题 (C/C++/Py/Java/Js/Go)

最佳信号覆盖问题 华为OD机试双机位C卷真题 - 华为OD上机考试双机位C卷真题 100分题型 华为OD机试双机位C卷真题目录点击查看: 华为OD机试双机位C卷真题题库目录|机考题库 + 算法考点详解 题目描述 模拟AP安装,将AP的位置投影到二维坐标系中,给出每个AP的WIFI信号强度,信号强度会随着距离的增加而减弱。给定: 第一行是2个整数N,D(N<=100,D<=100),其中N表示AP数量,D表示AP能够的信号能够覆盖的最大距离。接下来的N行里,每行包含3个整数x,y,s,表示这个AP在坐标系的位置为(x,y),x,y > 0,信号强度为s。所有坐标点是在X-Y坐标系内的整数坐标。为了简化计算,两个坐标之间的距离用切比雪夫距离表示(在二维空间内,两个点之间的切比雪夫距离为它们横坐标之差的绝对值与纵坐标之差的绝对值的最大值)。 需要你计算WIFI信号最好的坐标。 信号衰减计算方式: 如果第i个AP能到达(x,y),那么该AP在此处的信号为 ⌊s / (1

By Ne0inhk

轻量级C++ GIF动画生成库实战指南

轻量级C++ GIF动画生成库实战指南 【免费下载链接】gif-hSimple C++ one-header library for the creation of animated GIFs from image data. 项目地址: https://gitcode.com/gh_mirrors/gi/gif-h 想要在C++项目中轻松创建动态GIF动画吗?gif-h库正是你需要的利器!这个轻量级的单头文件库让GIF动画生成变得异常简单,只需几个函数调用就能让你的程序"动起来"。 🚀 三步快速上手 第一步:获取库文件 首先将gif-h库下载到你的项目中: git clone https://gitcode.com/gh_mirrors/gi/gif-h 第二步:基础框架搭建 创建一个基本的GIF动画只需要四个核心函数: #include <

By Ne0inhk
【C++篇】深入剖析C++ Vector底层源码及实现机制

【C++篇】深入剖析C++ Vector底层源码及实现机制

文章目录 须知 💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!  全面剖析vector底层及实现机制 接上篇:【C++篇】探索STL之美:vector容器讲解_c++vector容器-ZEEKLOG博客 前言  Vector是C++标准模板库(STL)中提供的一种动态数组容器,能够高效管理元素的存储与操作。它具有自动扩容的特性,即在存储空间不足时会自动分配更大的内存,保证连续存储的同时提高了灵活性。Vector支持随机访问,拥有接近数组的访问速度,同时也提供了丰富的成员函数用于插入、删除、排序等操作,兼顾了灵活性与性能。 总之,Vector是C++开发中最常用的容器之一,因其高效、灵活、易用的特性,在处理动态数据时显得尤为重要。 1. 基本结构与初始化(

By Ne0inhk