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
是可以的:
- 协程被挂起(当你到达
final_suspend
时)
- 没有人会在销毁后使用协程承诺(要特别确保没有引用该协程的类未来对象!)
destroy
之前没有调用过(双删)
在 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
是可以的:
- 协程被挂起(当你到达
final_suspend
时) - 没有人会在销毁后使用协程承诺(要特别确保没有引用该协程的类未来对象!)
destroy
之前没有调用过(双删)