接第二篇
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种表达:
- 协程可以使用“
co_return e;
”返回最终值e
。相当于Promise类型执行p.return_value(e) - 协程可以使用“
co_return
;” 不带任何值(或带 void 表达式)来结束没有最终值的协程。 - 不写任何co_return。
在2和3中,要确认协程是否结束,您可以调用h.done()
其协程句柄h
。执行coroutine_handle::done()
。注意不是coroutine_handle::operator bool()
,后者仅检查协程句柄是否包含指向协程内存的非空指针,而不检查执行是否完成。
这是一个新版本的 counter,其中counter
协程只生成 3 个值,而主函数只是不断打印值,直到协程完成。我们还需要进行一项更改 promise_type::final_suspend()
,但我们首先看一下新代码,然后讨论下面的 Promise 对象。
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_type
的final_suspend
有关系。我们来看下C 规范怎么解释这些函数的关系,如以下伪代码中:
{
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
//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
同样毫不奇怪,由于我们引发了越来越多的未定义行为,我们的程序很快就会崩溃。