【问题标题】:C++ coroutines: Is it valid to call `handle.destroy` from the final suspend point?C++ 协程:从最终挂起点调用“handle.destroy”是否有效?
【发布时间】:2021-05-05 13:04:14
【问题描述】:

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

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

不过,AddressSanitizer 仍为以下代码 sn-p 报告 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 中的错误正面?

【问题讨论】:

    标签: c++ clang c++20 address-sanitizer c++-coroutine


    【解决方案1】:

    如果你 100% 确定之后没有人会使用协程承诺,这是完全有效的。调用coroutine_handle::destroy相当于调用协程promise析构函数。

    如果是这样,那为什么要这样开始呢?只需从final_suspend返回std::suspend_never

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

    它等同于您的代码。如果我们想在协程完成后对协程承诺做一些有意义的事情,比如返回协程的存储结果,我们想暂停final_suspend 中的协程。由于您的 task 对象不存储或返回任何内容,我不明白为什么要最终暂停它。

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

    总之,如果:

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

    【讨论】:

    • 感谢您的回答。那么,您是否同意这是地址清理程序中的误报?
    • 对于你的问题“如果是这样,那为什么要这样开始呢?只需从final_suspend返回std::suspend_never”:上面的sn-p只是一个简化版本。您可以在godbolt.org/z/8Yadv1 中找到更完整的示例
    • 这很奇怪。我同意我不明白 TSAN 想要什么。有趣的是,稍微修改一下代码可以解决问题:godbolt.org/z/sjW6zs。我现在使用协程有很多经验,我可以告诉你,目前所有的编译器在正确编译协程时都存在重大问题。考虑到这个功能仍然被 clang 视为“体验”,我想这还不错?
    • 不幸的是,我确实需要await_suspend 来返回协程句柄以获得对称传输。我目前正在使用与您的编辑类似的解决方法(请参阅godbolt.org/z/zW3jTG
    • 我猜发生的事情是clang将await_suspend返回的协程句柄存储在协程框架本身内......在这种情况下,ASAN报告错误是正确的,给定协程框架被摧毁了。对于这个值,clang 应该使用堆栈而不是协程框架。听起来像一个铿锵的前端错误...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-30
    • 2020-06-27
    • 2017-06-11
    • 1970-01-01
    • 2019-05-24
    • 1970-01-01
    • 2023-02-10
    相关资源
    最近更新 更多