代码之家  ›  专栏  ›  技术社区  ›  Takatoshi Kondo

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

  •  1
  • Takatoshi Kondo  · 技术社区  · 1 年前

    概述

    当我使用BoostAsio的 CompletionToken 设施,我设计 completion_handler 生成自 CompletionToken 最终调用。

    但如果 完成处理程序 既不调用也不存储。在这种情况下, 完成处理程序 被简单地摧毁了。

    我已经使用C++20协程测试了该行为 use_awaitable CompletionToken。然后我观察到了有趣的行为。之后 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
    

    导螺杆连杆: 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
        );
    }
    
    1 回复  |  直到 1 年前
        1
  •  1
  •   sehe    1 年前

    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 |
    //                                                 |                 |
    //                                                 +-----------------+
    

    文档?

    我找到了Tanner Sansbury解释堆叠式协程的等效语义的旧答案[我的重点]:

    协程被挂起,直到操作完成并调用完成处理程序、io_service被销毁, 或Boost。Asio检测到合作已经暂停,无法恢复 ,此时Boost。阿西奥会破坏协同游戏。

    从…起 What does boost::asio::spawn do?