c++ - 在协程句柄上调用 destroy 会导致段错误

标签 c++ gcc segmentation-fault coroutine c++-coroutine

我最近开始使用 gcc-10 试验 C++ 协程。

下面的代码完全按照预期的方式执行,直到 main 退出,这会破坏 task 实例,导致 _coro.destroy(); 语句出现段错误。

为什么会出现这个段错误?

#include <iostream>
#include <coroutine>

class task
{
public:
  struct promise_type;
  using handle = std::coroutine_handle<promise_type>;
  struct promise_type
  {
    std::suspend_never initial_suspend() const noexcept { return {}; }
    std::suspend_never final_suspend() const noexcept { return {}; }
    auto get_return_object() noexcept { return task{handle::from_promise(*this)}; }
    void unhandled_exception() { std::terminate(); }
  };

  ~task() { if (_coro != nullptr) { std::cout << "Destroying task\n"; _coro.destroy(); } }

  task(task const &) = delete; // Not implemented for brevity...
  task &operator=(task &&other) = delete;
  task(task &&rhs) = delete;

private:
  handle _coro = nullptr;
  task(handle coro) : _coro{coro} { std::cout << "Created task\n"; }
};

std::coroutine_handle<> resume_handle;

struct pause : std::suspend_always
{
  void await_suspend(std::coroutine_handle<> h)
  {
    resume_handle = h;
  }
};

task go()
{
  co_await pause();

  std::cout << "Finished firing\n";
}

int main(int argc, char *argv[])
{
  auto g = go();

  resume_handle();

  return 0;
}

最佳答案

一些事情:

建议使用suspend_always或返回 coroutine_handlefinal_suspend恢复然后手动 .destroy()析构函数中的句柄。

~my_task() {
    if (handle) handle.destroy();
}

这是因为返回 suspend_never可能会导致句柄被破坏,如 Raymond Chen pointed out - 这意味着您需要手动跟踪句柄可能处于事件状态或可能被销毁的位置。


在我的例子中,段错误是由于双重释放,如果句柄已被复制到其他任何地方,就会发生这种情况。请记住 coroutine_handle<>是协程状态的非拥有句柄,因此多个拷贝可以指向同一(可能释放的)内存。

假设您没有进行任何类型的手动析构函数调用,请确保还强制在任何给定时间只使用一个句柄拷贝。

请记住,在很多情况下,复制语义将用于代替移动语义,因此您需要删除复制构造函数和赋值运算符。

您还必须定义移动构造函数,因为(根据 cppreference,所以有点保留)coroutine_handle<> 上的移动构造函数是隐式声明的,这意味着 内部指针只是被复制而不是设置为 nullptr (更多信息在 this answer 中提供)。

因此,默认移动构造函数将导致两个完全“有效”(因此bool(handle) == true)coroutine_handle<>对象,导致多个协程析构函数尝试 .destroy()单个协程实例,导致双重释放,从而导致潜在的段错误。

class my_task {
    coroutine_handle<> handle;
public:
    inline my_task(my_task &&o) : handle(o.handle) {
        o.handle = nullptr; // IMPORTANT!
    }

    ~my_task() {
        if (handle) handle.destroy();
    }

    my_task(const my_task &) = delete;
    my_task & operator =(const my_task &) = delete;
};

请注意,Valgrind 是此处用于调试此类错误的首选工具。在撰写本文时,Apt 存储库中提供的 Valgrind 有点过时并且不支持 io_uring 系统调用,因此它会因错误而阻塞。 Clone the latest master并构建/安装以获取 io_uring 的补丁。

关于c++ - 在协程句柄上调用 destroy 会导致段错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62981634/

相关文章:

c++ - 处理 SSL 客户端不读取所有数据

c++ - 在 C++ 中读取字符串数组 HDF5 属性

gcc - 在 64 位 Linux 上编译 32 位 at&t 汇编程序不起作用

c++ - 给定程序中的段错误

c++ - OpenCV,我得到分割但找不到它

c - 简单字符串操作中的段错误

c++ - 程序跳过用户输入

c++ - QuickFIX 处理修复消息的线程数

linux - 缓冲区溢出不运行

c++ - 输出不同导致蒙特卡洛积分结果不同