如果C++20协程既不调用也不存储完成处理程序,会发生什么?

66bbxpm5  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(182)

概述

当我使用BoostAsio的CompletionToken工具时,我设计从最终调用的CompletionToken生成completion_handler
但是我对completion_handler既没有被调用也没有被存储的行为感兴趣,在这种情况下,completion_handler只是被销毁了。
我已经使用use_awaitable CompletionToken测试了C++20协程的行为。然后我观察到有趣的行为。在completion_handler销毁路径执行co_await之后,堆栈被展开,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

注意

在实践中,我不使用“might uninvokable”token,我使用基于错误代码的方法。

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
    );
}

u4dcyp6a

u4dcyp6a1#

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));
          });
    }
  }


通常,所有权从一个处理程序转移到另一个处理程序,但是在这里,没有任何东西再引用它,所以它不再存在。
虽然这是实现细节,但它使得如果科罗永远无法恢复,引用计数将变为零。此外,沿着有一些不错的代码注解,您可能会发现有见地,例如,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 |
//                                                 |                 |
//                                                 +-----------------+

你呢?

我找到了一个更老的答案,坦纳·桑斯伯里解释了堆栈式协同程序的等效语义:
协程将被挂起,直到操作完成并调用完成处理程序,io_service被销毁,或Boost.Asio检测到协程已被挂起且无法恢复,此时Boost.Asio将销毁协程。

  • 从boost::asio::spawn做什么?*

相关问题