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

calling destroy on a coroutine handle causes segfault

我最近开始使用 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 或 return 一个 coroutine_handlefinal_suspend 恢复,然后手动 .destroy() 析构函数中的句柄。

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

这是因为 returning suspend_never 会导致 handle 被你破坏,就像 Raymond Chen - 这意味着你需要跟踪在哪里句柄可能还活着,也可能被手动销毁。


在我的例子中,段错误是由于 double-free,如果句柄已被 复制 到其他任何地方,就会发生这种情况。请记住 coroutine_handle<> 是协程状态的 non-owning 句柄,因此多个副本可以指向相同的 (potentially-freed) 内存。

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

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

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

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

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 和 build/install 获取 io_uring 的补丁。