C++ 协同程序:从最终挂起点调用“handle.destroy”是否有效?

C++ coroutines: Is it valid to call `handle.destroy` from the final suspend point?

在 C++ 协程的最终挂起中调用 handle.destroy() 是否有效?

据我了解,这应该没问题,因为协程当前已挂起,不会再次恢复。

仍然,AddressSanitizer 报告以下代码段的 heap-use-after-free

#include <experimental/coroutine>
#include <iostream>

using namespace std;

struct final_awaitable {
   bool await_ready() noexcept { return false; }
   void await_resume() noexcept {}
   template<typename PROMISE> std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle<PROMISE> coro) noexcept {
      coro.destroy(); // Is this valid?
      return std::experimental::noop_coroutine();
   }
};

struct task {
   struct promise_type;
   using coro_handle = std::experimental::coroutine_handle<promise_type>;

   struct promise_type {
      task get_return_object() { return {}; }
      auto initial_suspend() { return std::experimental::suspend_never(); }
      auto final_suspend() noexcept { return final_awaitable(); }
      void unhandled_exception() { std::terminate(); }
      void return_void() {}
   };
};

task foo() {
    cerr << "foo\n";
    co_return;
}

int main() {
   auto x = foo();
}

使用 clang 11.0.1 和编译标志编译时 -stdlib=libc++ --std=c++17 -fcoroutines-ts -fno-exceptions -fsanitize=address。 (参见 https://godbolt.org/z/eq6eoc

(我的实际代码的简化版。你可以在https://godbolt.org/z/8Yadv1中找到完整的代码)

这是我的代码中的问题还是 AddressSanitizer 中的错误肯定?

如果您 100% 确定之后没有人会使用协程承诺,那么它是完全有效的。调用 coroutine_handle::destroy 等同于调用协程 promise 析构函数。

如果是这样,那为什么要这样开始呢? return std::suspend_never 来自 final_suspend

std::suspend_never final_suspend() const noexcept { return {}; }

相当于你的代码。如果我们想在协程完成后对协程承诺做一些有意义的事情,比如 return 处理协程的存储结果,我们想在 final_suspend 中暂停协程。由于您的 task 对象不存储或 return 任何东西,我不明白为什么要最终挂起它。

请注意,如果您使用第三方库,例如我的 concurrencpp,您需要确保可以销毁不属于您的承诺。协程承诺可能会被暂停,但它的 coroutine_handle 仍会在其他地方引用。这可以追溯到第 1 点。对于我的图书馆,它不安全,因为它可能是 result 对象仍然引用它。

总之,在以下情况下调用 coroutine_promise::destroy 是可以的:

  1. 协程被挂起(当你到达 final_suspend 时)
  2. 没有人会在销毁后使用协程承诺(要特别确保没有引用该协程的类未来对象!)
  3. destroy之前没有调用过(双删)