C++ 20 Coroutine 协程

开发工具:Visual Studio 2019

概念

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

缺点

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

参考资料:https://blog.csdn.net/Woosual/article/details/107930147

CPU密集型代码(各种循环处理、计算等等):不需要频繁的切换线程,所以多线程是一个不错的选择。
IO密集型代码(文件处理、网络爬虫等):尤其是高并发时。为了保证公平,时间片的分配会越来越小,切换越发频繁。资源也就被浪费在了上下文切换中。为了解决 I/O 密集型运算内核在资源调度上的缺陷,所以引入了协程(coroutine)的概念。

如果在C++20的一个函数体内包含co_await、co_yield、co_return中任何一个关键字,那么这个函数就是一个coroutine。其中:

  • co_await:挂起当前的coroutine。
  • co_return:从当前coroutine返回一个结果。
  • co_yield:返回一个结果并且挂起当前的coroutine。

参考资料:https://blog.csdn.net/github_18974657/article/details/108526591

摘点关键的

了解了协程后我们就可以发现了以下事实:

一个线程只能有一个协程
协程函数需要返回值是Promise
协程的所有关键字必须在协程函数中使用
在协程函数中可以按照同步的方式去调用异步函数,只需要将异步函数包装在Awaitable类中,使用co_wait关键字调用即可。
知道了以上事实,我们就可以按照以下方式使用协程了:

在一个线程中同一个时间只调用一个协程函数,即只有一个协程函数执行完毕了,再去调用另一个协程函数。
使用Awatiable类包装所有的异步函数,一个异步函数处理一请求中的一部分工作(比如执行一次SQL查询,或者执行一次http请求等)。
在对应的协程函数中按照需要,通过增加co_wait关键字同步的调用这些异步函数。注意一个异步函数(包装好的Awaiable类)可以在多个协程函数中调用,协程函数可能在多个线程中被调用(虽然一个线程同一时间只调用一个协程函数),所以最好保证Awaiable类是线程安全的,避免出现需要加锁的情况。
在线程中通过调用不同的协程函数响应不同的请求。

代码

参考资料:https://blog.csdn.net/zhudonghe1/article/details/107035757

一个协程对象的完整结构如下

struct action                                           // 名称任意, 系统库提供了suspend_never, suspend_always, suspend_if可供调用
{
    bool await_ready() noexcept { return false; }       // 必须实现此接口
    void await_suspend(coroutine_handle<>) noexcept {}  // 必须实现此接口, 可通过此处在函数内部获取到handle
    void await_resume() noexcept {}                     // 必须实现此接口
}

template <typename ToOut, typename ToIn>    // 非必须
struct coroutine_name                       // 名称任意
{                       
    struct promise_type              // 名称必须为promise_type
    {
        ToOut _to_out;              // 非必须, 名称任意
        ToIn _to_in;                // 非必须, 名称任意

        promise_type() = default;   // 非必须
        ~promise_type() = default;  // 非必须

        coroutine_name get_return_object()  // 必须实现此接口
        {
            return std::coroutine_handle<promise_type>::from_promise(*this);
        }

        auto initial_suspend()              // 必须实现此接口, 返回值必须为类似action的struct
        {

        }

        auto final_suspend()                // 必须实现此接口, 返回值必须为类似action的struct
        {

        }

        void unhandled_exception()          // 必须实现此接口, 用于处理协程函数内部抛出错误
        {
            
        }

        auto yield_value(ToOut val)        // 如果协程函数内部有关键字co_yield则必须实现此接口, 返回值必须为类似action的struct
        {

        }

        void return_void()                 // 如果协程函数内部无关键字co_return则必须实现此接口
        {

        }
        
        void return_value(ToOut val)        // 如果协程函数内部有关键字co_return则必须实现此接口
        {
            _to_out = val;
        }
    }

    using promise_type = promise_name;          // 非必须,只是起别名,可以代替 coroutine_name 结构体中以下代码中的 promise_type
    std::coroutine_handle<promise_type> handle; // 非必须, 但一般均需实现, 名称随意, 提供给外面的handle
    coroutine_name(std::coroutine_handle<promise_type> p) : handle(coroutine_handle<promise_type>::from_promise(p))
    {

    };
}

coroutine_name func()           // 协程函数
{ 
    co_await suspend_always{};
    co_yield val;
    co_return val;
}

auto operator co_await(val_type &val) noexcept
{
    do something... 
    return action{};
}

int main()
{
    auto f = func();
    f.handle.resume();          //用得最多
    f.handle.promise()._to_out; //用得较多
    f.handle.done();            //用得较少
    f.handle.destory();         //一般不用
}

promise是C++对应协程规范的一种数据类型,里面有多个成员函数。通过它们,用户可以自定义协程的行为,如何时暂停、返回等

get_return_object         // to create return object
initial_suspend           // entering the coroutine body
return_value              // called when co_return called
return_void               // called before the end of coroutine body
yield_value               // called when co_yield called
final_suspend             // called when coroutine ends
unhandled_exception       // handle exception
wait_ready:返回 Awaitable 实例是否已经 ready 。协程开始会调用此函数,如果返回true,表示你想得到的结果已经得到了,协程不需要执行了。所以大部分情况这个函数的实现是要 return false。
await_suspend:挂起 awaitable 。该函数会传入一个 coroutine_handle 类型的参数。这是一个由编译器生成的变量。在此函数中调用 handle.resume(),就可以恢复协程。
await_resume:当协程重新运行时,会调用该函数。这个函数的返回值就是 co_await 运算符的返回值。

大致执行流程可以通过调试知道,第一步是调用 get_return_object (所以我们在这个函数实现中要创建返回对象),协程进入 initial_suspend-> 协程函数体 -> final_suspend 协程完全结束。
函数体中遇到 co_return 则调用 return_value。
initial_suspendfinal_suspend 函数中可以通过 return true or false 来决定是否暂停。

再来个文档资料吧

cppreference:https://en.cppreference.com/w/cpp/language/coroutines

再来个油管视频

Andreas Buhr: C++ Coroutines:https://youtu.be/vzC2iRfO_H8

我写的一个小例子,实现的是 C# 的 ContinueWith ,算不上真正的异步
我觉得写的挺烂的,就算用 thread::swap() 也可以更简单的实现,没什么参考价值

#include<iostream>
#include<future>
#include<thread>
#include<string>
#include<sstream>
#include<windows.h>
#include<coroutine>
#include<stdexcept>
#include<functional>

//获取线程ID
unsigned long long GetThreadId(std::thread::id tid)
{
	std::ostringstream oss;
	oss << tid;
	std::string stid = oss.str();
	return std::stoull(stid);
}

//输出字符串+线程ID
void Print(std::string s)
{
	std::string str = s + std::to_string(GetThreadId(std::this_thread::get_id())) + "\n\n";
	std::cout << str;
}

//用于追踪
//Just a little helper for debugging
struct LifetimeInspector
{
	LifetimeInspector(std::string s) :s(s)
	{
		std::cout << "Start: " << s << std::endl;
	}

	~LifetimeInspector()
	{
		std::cout << "End: " << s << std::endl;
	}

	std::string s;
};

/// <summary>
/// Task
/// The minimal machinery to use c++ 20 coroutines
/// </summary>
template<typename T>
struct Task
{
	/// <summary>
	/// TaskPromise:
	/// The minimal example coroutine
	/// </summary>
	struct TaskPromise
	{
		// to create return object
		Task get_return_object()
		{
			//from_promise 从协程的承诺对象创建 coroutine_handle
			return std::coroutine_handle<TaskPromise>::from_promise(*this);
		}

		//suspend_never 是空类,能用于指示 await 表达式绝不暂停并且不产生值
		// entering the coroutine body
		auto initial_suspend()
		{
			return std::suspend_never{};
		}

		//suspend_always 是空类,能用于指示 await 表达式始终暂停并且不产生值
		auto final_suspend()
		{
			return std::suspend_always{};
		}

		void return_value(T value)
		{
			//std::cout << "got " << value << "\n";
		}

		void unhandled_exception()
		{

		}
	};

	using promise_type = TaskPromise;
	//类模板 coroutine_handle 能用于指代暂停或执行的协程。
	std::coroutine_handle<TaskPromise> handle;
	Task(std::coroutine_handle<TaskPromise> h) :handle(h)
	{

	}
};

template<typename T>
struct Awaitable
{
        // _init 和 _result 没必要
	T _init;
	T _result;
	std::thread* _thread;

	Awaitable(T init, std::thread& t)
	{
		this->_init = init;
		this->_result = init;
		this->_thread = &t;
	}

	bool await_ready() const
	{
		return false;
	}

	T await_resume()
	{
		return this->_result;
	}

	void await_suspend(std::coroutine_handle<> handle)
	{
		//这里将协程句柄交给另外的线程,这样 co_await 之后的代码就会在新的线程上运行
		*this->_thread = std::thread([handle]()
			{
				handle.resume();
			});

                Print("Current Thread ID:");
		std::string str = "New Thread ID:" + std::to_string(GetThreadId(this->_thread->get_id())) + "\n\n";
		std::cout << str;
	}
};

Awaitable<int> SwitchToNewThread(std::thread& t)
{
	LifetimeInspector l("SwitchToNewThread");

	Awaitable<int> awaitable(100, t);
        //do something

	return awaitable;
}

Task<int> ResumingOnNewThread(std::thread& t)
{
	LifetimeInspector l("ResumingOnNewThread");
	Print("Current Thread ID:");
	co_await SwitchToNewThread(t);
	Print("After co_await,Current Thread ID:");

	co_return 42;
}

int main()
{
	DWORD start = GetTickCount64();

	LifetimeInspector l("main");
	Print("Current Thread ID:");

	std::thread t;
	ResumingOnNewThread(t);

	std::cout << "Done\n";

	DWORD end = GetTickCount64();

	std::cout << end - start << std::endl;

	Print("After Done,Current Thread ID:");

	t.join();
	//t.detach();
	
	return 0;
}

结果,可以看到 co_await 之后的代码确实是在新的线程上运行
注意:是 co_await 之后的代码在新线程上运行

代码执行顺序,通过调试观察
main() -> ResumingOnNewThread() 函数开头 -> get_return_object() -> Task() 构造器 -> get_return_object() -> 回到 ResumingOnNewThread()函数开头 -> initial_suspend() -> 回到 ResumingOnNewThread()函数开头 -> ResumingOnNewThread() 正常运行到 co_await SwitchToNewThread() -> SwitchToNewThread() 函数 -> Awaitable() 构造器 -> SwitchToNewThread() 函数执行完毕返回 co_await SwitchToNewThread()处 -> await_ready() -> 返回 co_await SwitchToNewThread()处 -> await_suspend() -> 返回 co_await SwitchToNewThread()处 之后正常运行

C++ 20 Coroutine 协程 结束

我主要是因为异步才去看 C++ 的协程,并没有完全掌握,只写了一个简单的异步实现例子,想学还是去油管找找吧,或者看一些国外的文档
但是很遗憾的,C++ 20 并不能像 C# 或其它语言的 aysnc/await 那样写出同步式的异步代码,C++ 20 的协程标准只包含编译器需要实现的底层功能,并没有包含简单方便地使用协程的高级库,相关的类和函数进入 std 标准库估计要等到 C++ 23 。所以,在 C++ 20 中,如果要使用协程,要么等别人封装好了给你用,要么就要自己学着用底层的功能自己封装。
所以 C++ 真正的异步标准库和规范要等到 C++ 23 了,三年又三年 😄

posted @ 2021-03-06 20:07  .NET好耶  阅读(1696)  评论(0编辑  收藏  举报