当我使用 BoostAsio 的 CompletionToken 工具时,我设计从最终调用的
completion_handler
生成 CompletionToken
。
但是我对
completion_handler
既没有被调用也没有被存储的行为感兴趣。在这种情况下,completion_handler
就被简单地销毁了。
我已经使用
use_awaitable
CompletionToken 测试了 C++20 协程的行为。然后我观察到有趣的行为。在 co_await
和 completion_handler
销毁路径执行之后,堆栈被展开并且 ioc.run()
在 int main()
中完成。在此过程中,不会捕获任何异常。
这是预期的行为还是未定义的行为?如果这是预期的行为,那么该文档在哪里?目前还没找到。
#include <iostream>
#include <boost/asio.hpp>
namespace as = boost::asio;
template <typename CompletionToken>
auto func(bool call, CompletionToken&& token) {
auto init =
[]
(
auto completion_handler,
bool call
) {
if (call) {
std::move(completion_handler)();
}
else {
// What is happend if completion_handler is neither invoked nor stored ?
}
};
return as::async_initiate<
CompletionToken,
void()
>(
init,
token,
call
);
}
struct trace {
trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
~trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
as::awaitable<void> proc1() {
trace t; // destructed correctly
try {
{
std::cout << "before call=true" << std::endl;
co_await func(true, as::use_awaitable);
std::cout << "after call=true" << std::endl;
}
{
std::cout << "before call=false" << std::endl;
co_await func(false, as::use_awaitable);
// the following part is never executed
std::cout << "after call=false" << std::endl;
}
}
catch (...) {
std::cout << "caught exception" << std::endl;
}
std::cout << "co_return" << std::endl;
co_return;
}
as::awaitable<void> proc2() {
for (int i = 0; i != 2; ++i) {
std::cout << "before proc1" << std::endl;
co_await proc1();
std::cout << "after proc1" << std::endl;
}
}
int main() {
as::io_context ioc;
as::co_spawn(ioc.get_executor(), proc2, as::detached);
ioc.run();
std::cout << "finish" << std::endl;
}
before proc1
trace::trace()
before call=true
after call=true
before call=false
trace::~trace()
finish
godbolt 链接:https://godbolt.org/z/3dzoYban6
在实践中,我不使用“可能不可调用”令牌。我使用基于错误代码的方法。
template <typename CompletionToken>
auto func(bool call, CompletionToken&& token) {
auto init =
[]
(
auto completion_handler,
bool call
) {
if (call) {
std::move(completion_handler)(boost::system::error_code{});
}
else {
// invoke with error code
std::move(completion_handler)(
boost::system::errc::make_error_code(
boost::system::errc::operation_canceled
)
);
}
};
return as::async_initiate<
CompletionToken,
void(boost::syste::error_code const&)
>(
init,
token,
call
);
}
completion_handler 类型为
auto init = [](auto completion_handler, bool should_complete) {
boost::asio::detail::awaitable_handler<boost::asio::any_io_executor> ch = std::move(completion_handler);
if (should_complete) {
std::move(ch)();
} else {
// What is happend if completion_handler is neither invoked nor stored ?
}
};
awaitable_handler
间接继承自awaitable_thread
,它在销毁时确实负责堆栈展开:
// Clean up with a last ditch effort to ensure the thread is unwound within
// the context of the executor.
~awaitable_thread()
{
if (bottom_of_stack_.valid())
{
// Coroutine "stack unwinding" must be performed through the executor.
auto* bottom_frame = bottom_of_stack_.frame_;
(post)(bottom_frame->u_.executor_,
[a = std::move(bottom_of_stack_)]() mutable
{
(void)awaitable<awaitable_thread_entry_point, Executor>(
std::move(a));
});
}
}
通常,所有权会从一个处理程序转移到另一个处理程序,但在这里,没有任何东西再引用它,因此它不再存在。
虽然这是实现细节,但它使得如果 coro 永远无法恢复,则引用计数将变为零。另外,一路上有一些不错的代码注释,您可能会发现它们很有洞察力,例如
impl/awaitable.hpp
开始于
// An awaitable_thread represents a thread-of-execution that is composed of one
// or more "stack frames", with each frame represented by an awaitable_frame.
// All execution occurs in the context of the awaitable_thread's executor. An
// awaitable_thread continues to "pump" the stack frames by repeatedly resuming
// the top stack frame until the stack is empty, or until ownership of the
// stack is transferred to another awaitable_thread object.
//
// +------------------------------------+
// | top_of_stack_ |
// | V
// +--------------+---+ +-----------------+
// | | | |
// | awaitable_thread |<---------------------------+ awaitable_frame |
// | | attached_thread_ | |
// +--------------+---+ (Set only when +---+-------------+
// | frames are being |
// | actively pumped | caller_
// | by a thread, and |
// | then only for V
// | the top frame.) +-----------------+
// | | |
// | | awaitable_frame |
// | | |
// | +---+-------------+
// | |
// | | caller_
// | :
// | :
// | |
// | V
// | +-----------------+
// | bottom_of_stack_ | |
// +------------------------------->| awaitable_frame |
// | |
// +-----------------+