推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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>
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 的协程从未离开过本线程!他只是给异步线程派发任务。


在这里插入图片描述


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐