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 了解 doneness 来解决此问题:

   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);
  ...
}