std::coroutine_handle<Promise>::done() 返回意外值
std::coroutine_handle<Promise>::done() returning unexpected value
我正在尝试为协程编写一个简单的循环调度程序。我的简化代码如下:
Generator<uint64_t> Counter(uint64_t i) {
for (int j = 0; j < 2; j++) {
co_yield i;
}
}
...
void RunSimple(uint64_t num_coroutines) {
std::vector<std::pair<uint64_t, Generator<uint64_t>>> gens;
for (uint64_t i = 0; i < num_coroutines; i++) {
// Storing coroutines into a vector is safe because we implemented the move
// constructor and move assigment operator for Generator.
gens.push_back({i, Counter(i)});
}
// This is a simple round-robin scheduler for coroutines.
while (true) {
for (auto it = gens.begin(); it != gens.end();) {
uint64_t i = it->first;
Generator<uint64_t>& gen = it->second;
if (gen) {
printf("Coroutine %lu: %lu.\n", i, gen());
it++;
} else {
// `gen` has finished, so remove its pair from the vector.
it = gens.erase(it);
}
}
// Once all of the coroutines have finished, break.
if (gens.empty()) {
break;
}
}
}
当我 运行 RunSimple(/*num_coroutines=*/4)
时,我得到以下输出:
Coroutine 0: 0.
Coroutine 1: 1.
Coroutine 2: 2.
Coroutine 3: 3.
Coroutine 0: 0.
Coroutine 1: 1.
Coroutine 2: 2.
Coroutine 3: 3.
Coroutine 1: 1.
Coroutine 3: 3.
Coroutine 3: 3.
输出的最后三行是意外的...协程 1 和 3 似乎没有在我期望的时候退出。经过进一步调查,发生这种情况是因为 std::coroutine_handle<Promise>::done()
为这两个协程返回了 false。你知道为什么这个方法返回 false 吗...我做错了什么吗?
编辑:这是我的 Generator
实现:
template <typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
T value_;
std::exception_ptr exception_;
Generator get_return_object() {
return Generator(handle_type::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template <std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From&& from) {
value_ = std::forward<From>(from);
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {
}
Generator(Generator&& g) : h_(std::move(g.h_)) { g.h_ = nullptr; }
~Generator() {
if (h_) {
h_.destroy();
}
}
Generator& operator=(Generator&& g) {
h_ = std::move(g.h_);
g.h_ = nullptr;
return *this;
}
explicit operator bool() {
fill();
return !h_.done();
}
T operator()() {
fill();
full_ = false;
return std::move(h_.promise().value_);
}
private:
bool full_ = false;
void fill() {
if (!full_) {
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
full_ = true;
}
}
};
您的程序包含未定义的行为。
问题是你的 fill
在 !full_
时恢复协程,不管协程是否在 final suspend
,你可以通过调用 h_.done()
了解.
Generator<uint64_t> Counter(uint64_t i) {
for (int j = 0; j < 2; j++) {
co_yield i;
}
// final suspend
}
如果你在最后的挂起点恢复协程,它会自行销毁,你不能再用你拥有的协程句柄做任何事情。
而你从operator bool
调用fill
,意思是当协程在final suspend时被挂起时被调用时,首先销毁协程,然后尝试访问它,这是UB :
fill(); // potentially destroy the coroutine
return !h_.done(); // access the destroyed coroutine
您可以通过让 fill
了解 done
ness 来解决此问题:
void fill() {
if (!h_.done() && !full_) {
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
full_ = true;
}
}
此外,您的移动赋值运算符泄漏了当前协程:
Generator& operator=(Generator&& g) {
h_ = std::move(g.h_); // previous h_ is leaked
g.h_ = nullptr;
return *this;
}
您可能希望在开始时使用类似 if (h_) h_.destroy();
的内容。
此外,如评论中所述,full_
成员必须在移动构造函数和赋值运算符中继承:
Generator(Generator&& g)
: h_(std::exchange(g.h_, nullptr))
, full_(std::exchange(g.full_, false)) {}
Generator& operator=(Generator&& g) {
full_ = std::exchange(g.full_, false);
...
}
我正在尝试为协程编写一个简单的循环调度程序。我的简化代码如下:
Generator<uint64_t> Counter(uint64_t i) {
for (int j = 0; j < 2; j++) {
co_yield i;
}
}
...
void RunSimple(uint64_t num_coroutines) {
std::vector<std::pair<uint64_t, Generator<uint64_t>>> gens;
for (uint64_t i = 0; i < num_coroutines; i++) {
// Storing coroutines into a vector is safe because we implemented the move
// constructor and move assigment operator for Generator.
gens.push_back({i, Counter(i)});
}
// This is a simple round-robin scheduler for coroutines.
while (true) {
for (auto it = gens.begin(); it != gens.end();) {
uint64_t i = it->first;
Generator<uint64_t>& gen = it->second;
if (gen) {
printf("Coroutine %lu: %lu.\n", i, gen());
it++;
} else {
// `gen` has finished, so remove its pair from the vector.
it = gens.erase(it);
}
}
// Once all of the coroutines have finished, break.
if (gens.empty()) {
break;
}
}
}
当我 运行 RunSimple(/*num_coroutines=*/4)
时,我得到以下输出:
Coroutine 0: 0.
Coroutine 1: 1.
Coroutine 2: 2.
Coroutine 3: 3.
Coroutine 0: 0.
Coroutine 1: 1.
Coroutine 2: 2.
Coroutine 3: 3.
Coroutine 1: 1.
Coroutine 3: 3.
Coroutine 3: 3.
输出的最后三行是意外的...协程 1 和 3 似乎没有在我期望的时候退出。经过进一步调查,发生这种情况是因为 std::coroutine_handle<Promise>::done()
为这两个协程返回了 false。你知道为什么这个方法返回 false 吗...我做错了什么吗?
编辑:这是我的 Generator
实现:
template <typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
T value_;
std::exception_ptr exception_;
Generator get_return_object() {
return Generator(handle_type::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template <std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From&& from) {
value_ = std::forward<From>(from);
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {
}
Generator(Generator&& g) : h_(std::move(g.h_)) { g.h_ = nullptr; }
~Generator() {
if (h_) {
h_.destroy();
}
}
Generator& operator=(Generator&& g) {
h_ = std::move(g.h_);
g.h_ = nullptr;
return *this;
}
explicit operator bool() {
fill();
return !h_.done();
}
T operator()() {
fill();
full_ = false;
return std::move(h_.promise().value_);
}
private:
bool full_ = false;
void fill() {
if (!full_) {
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
full_ = true;
}
}
};
您的程序包含未定义的行为。
问题是你的 fill
在 !full_
时恢复协程,不管协程是否在 final suspend
,你可以通过调用 h_.done()
了解.
Generator<uint64_t> Counter(uint64_t i) {
for (int j = 0; j < 2; j++) {
co_yield i;
}
// final suspend
}
如果你在最后的挂起点恢复协程,它会自行销毁,你不能再用你拥有的协程句柄做任何事情。
而你从operator bool
调用fill
,意思是当协程在final suspend时被挂起时被调用时,首先销毁协程,然后尝试访问它,这是UB :
fill(); // potentially destroy the coroutine
return !h_.done(); // access the destroyed coroutine
您可以通过让 fill
了解 done
ness 来解决此问题:
void fill() {
if (!h_.done() && !full_) {
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
full_ = true;
}
}
此外,您的移动赋值运算符泄漏了当前协程:
Generator& operator=(Generator&& g) {
h_ = std::move(g.h_); // previous h_ is leaked
g.h_ = nullptr;
return *this;
}
您可能希望在开始时使用类似 if (h_) h_.destroy();
的内容。
此外,如评论中所述,full_
成员必须在移动构造函数和赋值运算符中继承:
Generator(Generator&& g)
: h_(std::exchange(g.h_, nullptr))
, full_(std::exchange(g.full_, false)) {}
Generator& operator=(Generator&& g) {
full_ = std::exchange(g.full_, false);
...
}