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

2024-01-03 00:17:13 浏览数 (2)

接第二篇

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

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

我们来继续讨论协程和调用者的交互。这次看的是C 20协程自带的两个co_yield和co_return操作符,来简化上篇文章讨论的count3例子。

一、co_yield操作符

假设p是当前协程的 Promise 对象,则表达式“ co_yield e;”相当于“ co_await p.yield_value(e);” 。

co_yeild 用来简化couter3的例子,我们在ReturnObject4里面的promise_type添加一个方法yield_value,这个方法来将协程的值赋值给Promise。

新代码如下所示:

代码语言:javascript复制
struct ReturnObject4 {
  struct promise_type {
    unsigned value_;

    ReturnObject4 get_return_object() {
      return {
        .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::suspend_always yield_value(unsigned value) {
      value_ = value;
      return {};
    }
  };

  std::coroutine_handle<promise_type> h_;
};

ReturnObject4
counter4()
{
  for (unsigned i = 0;;   i)
    co_yield i;       // co yield i => co_await promise.yield_value(i)
}

void
main4()
{
  auto h = counter4().h_;
  auto &promise = h.promise();
  for (int i = 0; i < 3;   i) {
    std::cout << "counter4: " << promise.value_ << std::endl;
    h();
  }
  h.destroy();
}

输出:

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

二、co_return操作符

还记得这个系列的第一篇文章的例子吗,那时我们举了个couter的例子,这个couter协程在它的调用者main函数结束之后,还没有return结束。

Couter4的那个例子,协程里面的for循环是个无限循环没有终止条件,但是主函数循环3遍之后销毁了协程状态。那这里我们想让main调用者和协程同步:协程打印完所有有限数之后,main再退出来,要怎么做呢?

为了表示协程的结束,C 添加了一个新的co_return 运输符。co_return有3种表达:

  1. 协程可以使用“ co_return e;”返回最终值e。相当于Promise类型执行p.return_value(e)
  2. 协程可以使用“ co_return;” 不带任何值(或带 void 表达式)来结束没有最终值的协程。
  3. 不写任何co_return。

在2和3中,要确认协程是否结束,您可以调用h.done()其协程句柄h。执行coroutine_handle::done()。注意不是coroutine_handle::operator bool(),后者仅检查协程句柄是否包含指向协程内存的非空指针,而不检查执行是否完成。

这是一个新版本的 counter,其中counter 协程只生成 3 个值,而主函数只是不断打印值,直到协程完成。我们还需要进行一项更改 promise_type::final_suspend(),但我们首先看一下新代码,然后讨论下面的 Promise 对象。

代码语言:javascript复制
struct ReturnObject5 {
  struct promise_type {
    unsigned value_;

    ~promise_type() {
      std::cout << "promise_type destroyed" << std::endl;
    }
    ReturnObject5 get_return_object() {
      return {
        .h_ = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    std::suspend_always yield_value(unsigned value) {
      value_ = value;
      return {};
    }
    void return_void() {}
  };

  std::coroutine_handle<promise_type> h_;
};

ReturnObject5
counter5()
{
  for (unsigned i = 0; i < 3;   i)
    co_yield i;
  // falling off end of function or co_return; => promise.return_void();
  // (co_return value; => promise.return_value(value);)
}

void
main5()
{
  auto h = counter5().h_;
  auto &promise = h.promise();
  while (!h.done()) { // Do NOT use while(h) (which checks h non-NULL)
    std::cout << "counter5: " << promise.value_ << std::endl;
    h();
  }
  h.destroy();
}

输出:

代码语言:javascript复制
counter5: 0
counter5: 1
counter5: 2
promise_type destroyed

有几点需要注意

  • 使用 co_return要和 return_void或者return_value方法搭配使用,要不然是未定义的行为。这种代码很危险。
  • promise_type::return_void()promise_type::return_value(v)都返回 void;特别是它们不返回可等待的对象。
  • 还有一个重要的问题是在协程结束时要做什么。编译器是否应该更新协程状态并最后一次挂起协程,在co_return 之后,主函数中的代码还可以访问 Promise 对象并使用coroutine_handle吗?或者从协程返回是否应该主动破坏协程状态,例如隐式调用 coroutine_handle::destroy()?这个问题跟promise_typefinal_suspend有关系。我们来看下C 规范怎么解释这些函数的关系,如以下伪代码中:
代码语言:javascript复制
    {
        promise-type promise promise-constructor-arguments ;
        try {
            co_await promise.initial_suspend() ;
            function-body
        } catch ( ... ) {
            if (!initial-await-resume-called)
                throw ;
            promise.unhandled_exception() ;
        }
    final-suspend :
        co_await promise.final_suspend() ;
    }
    // "The coroutine state is destroyed when control flows
    //  off the end of the coroutine"

当协程返回时,函数隐式co_await处理promise.final_suspend()。如果 final_suspend确实挂起协程,则协程状态将最后一次更新并保持有效,并且协程外部的代码将负责通过调用协程句柄的方法来释放协程对象destroy()。如果final_suspend挂起协程,那么协程状态将自动销毁。

如果再也不想感知协程状态,那么没有理由为最后一次保存状态而担心关于手动释放协程状态,那么final_suspend()就返回 std::suspend_never。如果需要在协程返回后访问协程句柄或 Promise 对象,则需要 final_suspend()return std::suspend_always

在本例种,如果更改 ReturnObject5::promise_type::final_suspend()为 return std::suspend_never

代码语言:cpp复制
    //std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }

那么会输出:

代码语言:javascript复制
counter5: 0
counter5: 1
counter5: 2
promise_type destroyed
counter5: 2
Segmentation fault

第一个co_yield产生 0。第二个和第三个 co_yield产生 1 和 2,没有问题。然而,第三次我们恢复时h,执行到协程末尾脱落,破坏了协程状态。promise_type此时被销毁, h实际上留下了一个悬空指针。然后调用 h.done()这个悬空指针,引发了未定义的行为。有些机器上,未定义的行为恰好 h.done()返回 false。这会导致main5留在循环中并h()再次调用,只是这次它恢复垃圾而不是有效的协程状态。恢复垃圾不会 update promise.value_,仍然是 2。有些机器返回true,那么就不会打印2,

这时候输出就会如下:

代码语言:bash复制
counter5: 0
counter5: 1
counter5: 2
promise_type destroyed
Segmentation fault

同样毫不奇怪,由于我们引发了越来越多的未定义行为,我们的程序很快就会崩溃。

三、未完待续

0 人点赞