在讲协程之前,先回顾C11之前我们怎么处理多任务,怎么同步不同任务之间的处理顺序。想象一个你在用文本编辑器GUI,你对GUI的每个button进行操作,背后都有一段函数代码处理你的button事件。这就是事件驱动。事件驱动代码的一个典型示例是注册一个回调,每次套接字有数据要读取时都会调用该回调。
在更高级的事件驱动程序中,系统往往是这样设计,事件触发消息机制,发生消息给处理函数处理。一旦阅读了整个消息,可能在多次调用之后,就可以解析该消息并从更高的抽象层调用另一个回调,依此类推。编写这种代码很痛苦,因为必须将代码分解为一堆不同的函数。它们是不同的函数,所以不共享局部变量。
C++20在语言层面上支持协程,这极大地改进编写事件驱动代码的过程。
这篇文章会先探索C++20协程,之后会举例说明这个事件驱动如何用协程优雅地完成。
粗略地说,协程是可以互相调用但不共享堆栈的函数,因此可以在任何时候灵活地暂停执行以进入不同的协程。C++ 协程经常使用术语future和 Promise来解释。这些术语与std::future
和std::promise
并没有关系。
C++20 提供了一个新的操作符,叫做co_await
。
解释如下,代码co_await a;
执行以下操作:
co_await
)目标对象a
的方法 ,并将步骤 2 中的可调用对象传递给该方法。这里注意到,步骤 3 中的方法返回时不会将控制权返回给协程。仅当调用步骤 2 中的可调用函数时,协程才会恢复执行。
如前所述,newco_await
运算符确保函数的当前状态捆绑在堆上的某个位置,并创建一个可调用对象,该对象的调用将继续执行当前函数。可调用对象的类型为 std::coroutine_handle<>
。
协程句柄的行为很像 C 指针。它可以很容易地复制,但它没有析构函数来释放与协程状态相关的内存。为了避免内存泄漏,通常必须通过调用该 coroutine_handle::destroy
方法来销毁协程状态(协程可以在完成时销毁自身,但是这个协程是个死循环,所以要显式调用destroy方法)。与 C 指针一样,一旦协程句柄被销毁,引用同一协程的协程句柄将指向垃圾内存(野指针)并在调用时表现出未定义的行为。协程句柄对于协程的整个执行都是有效的,即使控制多次流入和流出协程也是如此。
从例子开始
- 声明一个函数(协程)。辨别协程函数的要点是有一个co_await操作符,操作符上面和下面的代码不会被cpu连续执行到。这个协程函数携带一个参数std::coroutine_handle<>,并返回一个ReturnObject。
- 调用这个std::coroutine_handle<>
-销毁这个协程函数
#include <concepts>
#include <coroutine>
#include <exception>
#include <iostream>
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {}
};
};
struct Awaiter {
std::coroutine_handle<> *hp_;
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) { *hp_ = h; }
constexpr void await_resume() const noexcept {}
};
#define COLOR_RED "\033[21;31m"
#define COLOR_RED_BOLD "\033[1;31m"
#define COLOR_GREEEN "\033[21;32m"
#define COLOR_GREEEN_BOLD "\033[1;32m"
#define COLOR_END "\033[0m"
ReturnObject
counter(std::coroutine_handle<> *continuation_out)
{
Awaiter a{continuation_out};
std::cout << COLOR_RED_BOLD << "[counter func][enter into]" << COLOR_END << std::endl;
for (unsigned i = 0;; ++i) {
std::cout << COLOR_RED << "[counter func][begin] counter:" << i << COLOR_END << std::endl;
co_await a;
std::cout << COLOR_RED << "[counter func][end ] counter:" << i << COLOR_END << std::endl;
}
std::cout << COLOR_RED_BOLD << "[counter func][leave]" << COLOR_END << std::endl;
}
void
main1()
{
std::cout << COLOR_GREEEN_BOLD << "[main1 func ][enter into]" << COLOR_END << std::endl;
std::coroutine_handle<> h;
counter(&h);
for (int i = 0; i < 3; ++i) {
std::cout << COLOR_GREEEN << "[main1 func ][begin] i: " << i << COLOR_END << std::endl;
h();
std::cout << COLOR_GREEEN << "[main1 func ][end ] i: " << i << COLOR_END << std::endl;
}
h();
h();
h.destroy();
std::cout << COLOR_GREEEN_BOLD << "[main1 func ][leave]" << COLOR_END << std::endl;
}
输出:
g++(需要g++10以上版本)编译器使用-fcoroutines选项来编译协程代码
g++-10 -fcoroutines -std=c++20
这里我们看到几个现象:
当第一次执行到表达式时 co_await a,编译器会创建一个协程句柄并将其传递给该方法 a.await_suspend(coroutine_handle)
。类型a必须支持某些方法,有时称为“可等待”对象或“等待者”。
这里的await_suspend()
每次被调用时都会存储协程句柄 *hp_=h
,但该句柄不会在调用过程中发生变化。(回句柄就像指向协程状态的指针,因此虽然值可能会发生变化,但指针本身保持不变。),因此改写成:
void
Awaiter::await_suspend(std::coroutine_handle<> h)
{
if (hp_) {
*hp_ = h;
hp_ = nullptr;
}
}
Awaiter
还有另外两种方法。这些方法是语言所要求的。
await_ready
是一种优化。如果返回 true
,则co_await
不会暂停该函数。比如说我将return false改成return true。这个例子的协程就不会停止。会一直打印:
当然,改写 await_suspend
恢复(或不挂起)当前协程来实现相同的效果。比如说这种写法:
constexpr bool await_ready() const noexcept { return false; }
bool await_suspend(std::coroutine_handle<> h) {
*hp_= h;
return false;
}
这里说明的是await_suspend其实有3种函数重载。他们3个区别在于返回值不一样,这里改写的是其中一种类型,返回bool。如果返回true,则挂起当前协程兵返回给当前协程的调用者,否则则直接恢复当前协程。像之前那种类型直接返回void,是指直接返回给协程的调用者。
但这里考虑到性能,因为进入await_suspend
编译器必须将所有状态捆绑到协程句柄引用的堆对象中,代价可能会很昂贵。
await_resume
返回void
,但如果它返回一个值,则该值将是表达式的值 co_await
。
<coroutine>
头文件提供了两个预定义的等待者,std::suspend_always
和std::suspend_never
. 顾名思义,suspend_always::await_ready
always 返回 false,而suspend_never::await_ready
always 返回 true。
ReturnObject类型是必须通过co_await才能访问的。
这里的 counter 是一个永远计数的函数,递增并打印一个无符号整数。尽管代码很简单,但该例的有意思的点在于,即使控制变量i
和couter
调用它的函数之间反复切换,变量也能保持其值。
在此main1
示例中,调用counter
并使用std::coroutine_handle<>*
,它们在Awaiter
类型中。其中await_suspend
方法中,该类型存储co_await
生成的协程句柄。每次main1
调用协程句柄时,它都会再次触发循环迭代,直到再次遇到co_await
该语句处挂起。
本文通过例子讲解了Awaiter对象实战,以及从实践讲到Awaiter的3个必要要素。
我们利用concept特性来表达这个概念
template<typename A>
concept Awaitable = requires(GetAwaiter_t<A> awaiter, coroutine_handle<>handle) {
{awaiter.await_ready()}->convertible_to<bool>;
awaiter.await_suspend(handle);
awaiter.await_resume();
}
c++20的协程学习记录(二): 初探ReturnObject和Promise
https://cloud.tencent.com/developer/article/2375995
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。