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_suspend
和 final_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 了,三年又三年 😄