在协程句柄上调用 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_handle
从 final_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) == true
)coroutine_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 的补丁。
我最近开始使用 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_handle
从 final_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) == true
)coroutine_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 的补丁。