前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >c++20的协程学习记录(一): 初探co_await和std::coroutine_handle<>

c++20的协程学习记录(一): 初探co_await和std::coroutine_handle<>

原创
作者头像
mariolu
修改2024-01-02 09:03:31
4380
修改2024-01-02 09:03:31
举报

一、事件驱动

在讲协程之前,先回顾C11之前我们怎么处理多任务,怎么同步不同任务之间的处理顺序。想象一个你在用文本编辑器GUI,你对GUI的每个button进行操作,背后都有一段函数代码处理你的button事件。这就是事件驱动。事件驱动代码的一个典型示例是注册一个回调,每次套接字有数据要读取时都会调用该回调。

在更高级的事件驱动程序中,系统往往是这样设计,事件触发消息机制,发生消息给处理函数处理。一旦阅读了整个消息,可能在多次调用之后,就可以解析该消息并从更高的抽象层调用另一个回调,依此类推。编写这种代码很痛苦,因为必须将代码分解为一堆不同的函数。它们是不同的函数,所以不共享局部变量。

二、C++20的协程

C++20在语言层面上支持协程,这极大地改进编写事件驱动代码的过程。

这篇文章会先探索C++20协程,之后会举例说明这个事件驱动如何用协程优雅地完成。

2.1 协程

粗略地说,协程是可以互相调用但不共享堆栈的函数,因此可以在任何时候灵活地暂停执行以进入不同的协程。C++ 协程经常使用术语futurePromise来解释。这些术语与std::futurestd::promise并没有关系。

C++20 提供了一个新的操作符,叫做co_await

2.1.1 co_await

解释如下,代码co_await a;执行以下操作:

  1. 确保当前函数(必须是程)中的所有局部变量都保存到堆分配的对象中。
  2. 创建一个可调用对象,在调用该对象时,将在表达式co_await之后立即恢复协程的执行 。
  3. 调用(或更准确地说跳转到co_await)目标对象a的方法 ,并将步骤 2 中的可调用对象传递给该方法。

这里注意到,步骤 3 中的方法返回时不会将控制权返回给协程。仅当调用步骤 2 中的可调用函数时,协程才会恢复执行。

2.1.3 协程句柄std::coroutine_handle<>

如前所述,newco_await运算符确保函数的当前状态捆绑在堆上的某个位置,并创建一个可调用对象,该对象的调用将继续执行当前函数。可调用对象的类型为 std::coroutine_handle<>

协程句柄的行为很像 C 指针。它可以很容易地复制,但它没有析构函数来释放与协程状态相关的内存。为了避免内存泄漏,通常必须通过调用该 coroutine_handle::destroy方法来销毁协程状态(协程可以在完成时销毁自身,但是这个协程是个死循环,所以要显式调用destroy方法)。与 C 指针一样,一旦协程句柄被销毁,引用同一协程的协程句柄将指向垃圾内存(野指针)并在调用时表现出未定义的行为。协程句柄对于协程的整个执行都是有效的,即使控制多次流入和流出协程也是如此。

2.2 使用方法

从例子开始

- 声明一个函数(协程)。辨别协程函数的要点是有一个co_await操作符,操作符上面和下面的代码不会被cpu连续执行到。这个协程函数携带一个参数std::coroutine_handle<>,并返回一个ReturnObject。

- 调用这个std::coroutine_handle<>

-销毁这个协程函数

代码语言:javascript
复制
#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;
}

输出:

2.2.1 g++编译代码

g++(需要g++10以上版本)编译器使用-fcoroutines选项来编译协程代码

代码语言:javascript
复制
g++-10 -fcoroutines -std=c++20

2.2.2 现象

这里我们看到几个现象:

  • counter的指永远都在递增,说明虽然是不同协程,这个counter虽然为函数局部变量,但是他的值一直存在着
  • 第二个现象是 函数执行顺序 是 main1 => 函数指针std::coroutine_handle<> h() ==> counter => 遇到 co_await ==> 遇到下一次co_await停止 ==> 继续执行main1 => 继续执行第2个std::coroutine_handle<> h() ==> .... ==> main1 leave
  • 第三个现象是 main函数退出时,counter function还没有执行完

2.2.3 初步结论

  • await_suspend()

当第一次执行到表达式时 co_await a,编译器会创建一个协程句柄并将其传递给该方法 a.await_suspend(coroutine_handle)。类型a必须支持某些方法,有时称为“可等待”对象或“等待者”。

这里的await_suspend()每次被调用时都会存储协程句柄 *hp_=h,但该句柄不会在调用过程中发生变化。(回句柄就像指向协程状态的指针,因此虽然值可能会发生变化,但指针本身保持不变。),因此改写成:

代码语言:javascript
复制
void
Awaiter::await_suspend(std::coroutine_handle<> h)
{
  if (hp_) {
    *hp_ = h;
    hp_ = nullptr;
  }
}
  • await_read

Awaiter还有另外两种方法。这些方法是语言所要求的。

await_ready是一种优化。如果返回 true,则co_await不会暂停该函数。比如说我将return false改成return true。这个例子的协程就不会停止。会一直打印:

当然,改写 await_suspend恢复(或不挂起)当前协程来实现相同的效果。比如说这种写法:

代码语言:cpp
复制
  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

await_resume返回void,但如果它返回一个值,则该值将是表达式的值 co_await

<coroutine>头文件提供了两个预定义的等待者,std::suspend_alwaysstd::suspend_never. 顾名思义,suspend_always::await_readyalways 返回 false,而suspend_never::await_readyalways 返回 true。

  • ReturnObject

ReturnObject类型是必须通过co_await才能访问的。

  • 代码串起来

这里的 counter 是一个永远计数的函数,递增并打印一个无符号整数。尽管代码很简单,但该例的有意思的点在于,即使控制变量i couter调用它的函数之间反复切换,变量也能保持其值。

在此main1示例中,调用counter并使用std::coroutine_handle<>*,它们在Awaiter类型中。其中await_suspend方法中,该类型存储co_await生成的协程句柄。每次main1调用协程句柄时,它都会再次触发循环迭代,直到再次遇到co_await该语句处挂起。

三、总结

3.1 await对象

本文通过例子讲解了Awaiter对象实战,以及从实践讲到Awaiter的3个必要要素。

我们利用concept特性来表达这个概念

代码语言:cpp
复制
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();
}

3.2 Await流程图

四、未完待续

c++20的协程学习记录(二): 初探ReturnObject和Promise

https://cloud.tencent.com/developer/article/2375995

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、事件驱动
    • 二、C++20的协程
      • 2.1.1 co_await
      • 2.1.3 协程句柄std::coroutine_handle<>
  • 2.1 协程
    • 2.2 使用方法
      • 2.2.1 g++编译代码
      • 2.2.2 现象
      • 2.2.3 初步结论
  • 三、总结
    • 3.1 await对象
      • 3.2 Await流程图
      • 四、未完待续
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档