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

2024-01-03 08:04:36 浏览数 (2)

接上篇我们讨论到了Awaiter对象:

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

(https://cloud.tencent.com/developer/article/2375979)

这边继续讨论协程返回对象

一、协程返回对象ReturnObject

上篇讨论的counter返回类型。协程在语言层面上有限制允许的返回类型。具体来说,协程的返回类型必须具有嵌套的类型R::promise_type,R::promise_type必须包含方法get_return_object()返回这个类型 。在许多关于协程的讨论中,返回类型被称为 future,但为了清楚起见,这里还是将其称为ReturnObject2

1.1 ReturnObject包含coroutine_handle<>

1.1.1 从coroutine_handle得到Promise类型

这里使用方法get_return_object()coroutine_handle<>*得到ReturnObject2,而不用上章节用的传递coroutine_handle<>counter

实际上coroutine_handle包含了promise_type,并且可以通过一个固定偏移量计算出来。因此声明如下

代码语言:javascript复制
    template<class Promise = void> struct coroutine_handle;

1.1.2 从Promise类型得到coroutine_handle

std::coroutine_handle<T>任何类型的 T 都可以隐式转换为 std::coroutine_handle<void>。非空类型允许在协程句柄和promise_type之间来回转换。具体来说,在可以使用静态方法coroutine_handle::from_pomise通过Promise类型取协程句柄:

代码语言:javascript复制
    // from within a method of promise_type
    std::coroutine_handle<promise_type>::from_promise(*this)

1.2 示例

现在我们已经拥有将协程句柄粘贴到新函数的返回对象中所需的一切counter2。这是修改后的示例:

代码语言:cpp复制
#include <concepts>
#include <coroutine>
#include <exception>
#include <iostream>

struct ReturnObject2 {
  struct promise_type {
    ReturnObject2 get_return_object() {
      return {
        // Uses C  20 designated initializer
        .h_ = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
  };

  std::coroutine_handle<promise_type> h_;
  operator std::coroutine_handle<promise_type>() const { return h_; }
  // A coroutine_handle<promise_type> converts to coroutine_handle<>
  operator std::coroutine_handle<>() const { return h_; }
};

ReturnObject2
counter2()
{
  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 std::suspend_always{};
    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
main2()
{
  std::cout << COLOR_GREEEN_BOLD << "[main2 func  ][enter into]" << COLOR_END << std::endl;
  std::coroutine_handle<> h = counter2();
  for (int i = 0; i < 3;   i) {
    std::cout << COLOR_GREEEN << "[main2 func  ][begin] i:      " << i << COLOR_END << std::endl;
    h();
    std::cout << COLOR_GREEEN << "[main2 func  ][end  ] i:      " << i << COLOR_END << std::endl;
  }
  h.destroy();
  std::cout << COLOR_GREEEN_BOLD << "[main2 func  ][leave]" << COLOR_END << std::endl;
}

关于上面的代码有几点变化。

  • 首先,因为将coroutine_handle句柄放入返回对象中,因此不再需要调用者来保存协程句柄,因此调用者只需运行co_await std::suspend_always{}.
  • 其次,请注意,返回对象counter2()超出了范围,并且在std::coroutine_handle<> h =counter2();

被销毁。但是 coroutine_handle h就像一个 C 指针,而不像一个对象。已经销毁了包含 的对象ReturnObject2::h_不重要 ,因为已经将指针复制到了 中 h。所以在 main2最后通过过调用 h.destroy()来销毁。所以这里代码调用counter2()并不能忽略返回值,否则会造成内存泄漏。

二、Promise类型

到目前为止,实例围绕主函数和协程之间的来回传递控制,这里也没有传递任何数据。

所以现在要开始讲一个例子教你如果从countermain 交换数据。

2.1 Promise传输给mian函数

协程状态promise_type里面加一个字段value_,并使用该字段将值从协程传输到main函数。

首先Promise类型通过std::coroutine_handle<ReturnObject3::promise_type>藏匿在coroutine_handle,之后通过协程句柄的方法promise()将返回promise_type&

2.2 协程数据传输给Promise

协程如何获得自己的 Promise 对象?

上个实例中Awaiter我们有讲到Awaiter::await_suspendawait_ready 。这里只需要做点小改造,让await_ready返回false,让代码Awaiter::await_suspend

有执行机会,并在里面对promise进行赋值。

2.3 改造

这里定义了一个新的等待者类型GetPromise,其中包含一个字段promise_type *p_。我们让它的 await_suspend方法将 Promise 对象的地址存储在 中p_,但随后返回 false 以避免实际挂起协程。然后 co_await类型的表达式void。这次,我们希望co_await返回 Promise 对象的地址,因此我们还添加了一个await_resume返回 的函数 p_

代码语言:javascript复制
template<typename PromiseType>
struct GetPromise {
  PromiseType *p_;
  bool await_ready() { return false; } // says yes call await_suspend
  bool await_suspend(std::coroutine_handle<PromiseType> h) {
    p_ = &h.promise();
    return false;     // says no don't suspend coroutine after all
  }
  PromiseType *await_resume() { return p_; }
};

除了void和之外boolawait_suspend还可能返回 a coroutine_handle,在这种情况下,返回的句柄将立即恢复,但可能会降低效率。

这是我们的新计数器代码,其中主函数打印出协程返回的计数器值:

代码语言:cpp复制
#include <concepts>
#include <coroutine>
#include <exception>
#include <iostream>

struct ReturnObject3 {
  struct promise_type {
    unsigned value_;

    ReturnObject3 get_return_object() {
      return ReturnObject3 {
        .h_ = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
  };

  std::coroutine_handle<promise_type> h_;
  operator std::coroutine_handle<promise_type>() const { return h_; }
};

ReturnObject3
counter3()
{
  auto pp = co_await GetPromise<ReturnObject3::promise_type>{};

  for (unsigned i = 0;;   i) {
    pp->value_ = i;
    co_await std::suspend_always{};
  }
}

void
main3()
{
  std::coroutine_handle<ReturnObject3::promise_type> h = counter3();
  ReturnObject3::promise_type &promise = h.promise();
  for (int i = 0; i < 3;   i) {
    std::cout << "counter3: " << promise.value_ << std::endl;
    h();
  }
  h.destroy();
}

输出:

代码语言:javascript复制
counter3: 0
counter3: 1
counter3: 2

这里 Promise 对象 通过将i值复制,来将协程的值promise_type::value_传输到主函数。这里也可以创建value_一个 并返回一个指向内部变量的 指针unsigned *。之所以可以这样做,是因为协程的局部变量位于堆中的协程状态对象内,因此它们的内存在co_await()调用期间保持有效, 直到有人调用协程句柄的destroy()为止。将粘在返回对象内部会更方便 ,但是不幸的是,考虑到返回对象的构造方式,没有优雅的方法可以做到这一点。icounter3co_awaitdestroy()&i

三 未完待续

c 20的协程学习记录(三): co_yield和co_return操作符

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

0 人点赞