【问题标题】:boost::asio::yield_context: unexpected forced_unwind exceptionboost::asio::yield_context:意外的forced_unwind异常
【发布时间】:2015-01-15 22:43:32
【问题描述】:

我正在尝试为 boost::asio 编写自定义异步函数,如 here 所述。

但是我得到 boost::coroutines::detail::forced_unwind 异常与 result.get

#include <boost/chrono.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>

#include <iostream>

namespace asio = ::boost::asio;


template <typename Timer, typename Token>
auto my_timer (Timer& timer, Token&& token)
{
  typename asio::handler_type<Token,
      void (::boost::system::error_code const)>::type
      handler (std::forward<Token> (token));

  asio::async_result<decltype (handler)> result (handler);

  timer.async_wait (handler);
  return result.get (); // Got forced_unwind exception here.
}

int main ()
{
  asio::io_service io;
  asio::steady_timer timer (io, ::boost::chrono::seconds (1));

  asio::spawn (io, [&] (asio::yield_context yield)
      {
      try {
        std::cout << "my_timer enter\n";
        my_timer (timer, yield);
        std::cout << "my_timer returns\n";
      }
      catch (const boost::coroutines::detail::forced_unwind& e)
      { 
        std::cout << "boost::coroutines::detail::forced_unwind\n"; 
      }
    }
  );

  io.run ();
}

Coliru 上的代码相同

更新

行为存在于:

Darwin 14.0.0 (MacOS 10.10) 
clang version 3.6.0 (trunk 216817) and gcc version 4.9.1 (MacPorts gcc49 4.9.1_1) 
boost 1.57

Red Hat 6.5
gcc version 4.7.2 20121015 (Red Hat 4.7.2-5) (GCC)
boost 1.57 and 1.56
(the example code was trivially modified because gcc 4.7 does not support c++14 mode)

【问题讨论】:

    标签: c++ boost boost-asio coroutine


    【解决方案1】:

    简而言之,您需要创建处理程序的副本,例如通过将其发布到io_service,然后再尝试获取async_result 以保持协程存活。


    Boost.Asio 通过销毁协程来防止不可恢复的协程无限期挂起,从而导致协程的堆栈展开。协程对象在其销毁过程中将抛出boost::coroutines::detail::forced_unwind,从而导致挂起的堆栈展开。 Asio 通过以下方式实现了这一目标:

    • yield_contextCompletionToken 为协程维护一个weak_ptr
    • 在构造专用的handler_type::type处理程序时,它通过CompletionToken的weak_ptr为协程获取一个shared_ptr。当处理程序作为完成处理程序传递给异步操作时,将复制处理程序及其shared_ptr。当处理程序被调用时,它会恢复协程。
    • 在调用async_result::get() 时,特化将重置在构造过程中传递给async_result 的处理程序拥有的协程shared_ptr,然后产生协程。

    这里试图说明代码的执行。 | 中的路径表示活动堆栈,: 表示挂起的堆栈,箭头表示控制权的转移:

    boost::asio::io_service io_service;
    boost::asio::spawn(io_service, &my_timer);
    `-- dispatch a coroutine creator
        into the io_service.
    io_service.run();
    |-- invoke the coroutine entry
    |   handler.
    |   |-- create coroutine
    |   |   (count: 1)
    |   |-- start coroutine        ----> my_timer()
    :   :                                |-- create handler1 (count: 2)
    :   :                                |-- create asnyc_result1(handler1)
    :   :                                |-- timer.async_wait(handler)
    :   :                                |   |-- create handler2 (count: 3)
    :   :                                |   |-- create async_result2(handler2)
    :   :                                |   |-- create operation and copy
    :   :                                |   |   handler3 (count: 4)
    :   :                                |   `-- async_result2.get()
    :   :                                |       |-- handler2.reset() (count: 3)
    |   `-- return                 <---- |       `-- yield
    |       `-- ~entry handler           :
    |           (count: 2)               :
    |-- io_service has work (the         :
    |   async_wait operation)            :
    |   ...async wait completes...       :
    |-- invoke handler3                  :
    |   |-- resume                 ----> |-- async_result1.get()
    :   :                                |   |--  handler1.reset() (count: 1)
    |   `-- return                 <---- |   `-- yield
    |       `-- ~handler3                :       :
    |           |  (count: 0)            :       :
    |           `-- ~coroutine()   ----> |       `-- throw forced_unwind
    

    为了解决这个问题,需要在恢复协程时通过asio_handler_invoke() 复制和调用handler。例如,下面将发布一个完成处理程序1io_service,它调用handler 的副本:

    timer.async_wait (handler);
    
    timer.get_io_service().post(
      std::bind([](decltype(handler) handler)
      {
        boost::system::error_code error;
        // Handler must be invoked through asio_handler_invoke hooks
        // to properly synchronize with the coroutine's execution
        // context.
        using boost::asio::asio_handler_invoke;
        asio_handler_invoke(std::bind(handler, error), &handler);
      }, handler)
    );
    return result.get ();
    

    正如here 演示的那样,使用此附加代码,输出变为:

    my_timer enter
    my_timer returns
    

    1。完成处理程序代码可能会被清理一下,但是当我回答 how to resume a Boost.Asio stackful coroutine from a different thread 时,我观察到一些编译器选择了错误的 asio_handler_invoke 钩子。

    【讨论】:

    • 感谢您的回答。喝了几杯咖啡后,我明白了为什么在协程破坏后总是调用发布的 std::bind 处理程序,即使在多线程 io_service 执行环境中也是如此 :) 但是,在我看来,这样的设计过于复杂并且容易出错 :(
    • 我还找到了一个替代解决方案:使用包含处理程序副本的包装器,从而防止早期协程破坏。代码:coliru.stacked-crooked.com/a/b639954744728c43你怎么看?合理吗?
    • @Nikki 很高兴为您提供帮助。协程销毁后不调用std::bind处理程序;相反,它使协程保持活动状态,因为它有一个handler 的副本,其中包含一个shared_ptr 到协程。我并不清楚预期的目标,所以我发布的代码集中在演示正确的处理程序调用和协程产生。您发布的最新代码在功能上非常不同。期望的目标是什么?当一个异步操作启动时,你希望能够在协程中yield之前做额外的工作,还是应该立即yield?
    【解决方案2】:

    这是一个 Boost Coroutine 的实现细节。

    如此处所述:exceptions

    ⚠重要

    coroutine-function 执行的代码不能阻止detail::forced_unwind exception 的传播。吸收该异常将导致堆栈展开失败。因此,任何捕获所有异常的代码都必须重新throw 任何待处理的detail::forced_unwind 异常。

    因此,您明确需要通过此异常。显式编码处理程序,如:

    Live On Coliru

    try {
      std::cout << "my_timer enter\n";
      my_timer(timer, yield);
      std::cout << "my_timer returns\n";
    }
    catch (boost::coroutines::detail::forced_unwind const& e)
    { 
       throw; // required for Boost Coroutine!
    }
    catch (std::exception const& e)
    { 
       std::cout << "exception '" << e.what() << "'\n";
    }
    

    这个特殊的例外是一个实现细节,必须

    • 在协程上下文中预期
    • 不要吞下,否则会违反 RAII 语义,从而导致资源泄漏以及您的 RAII 类型可能出现未定义的行为。

    公平地说,这使得“天真地”使用可能无法提供此保证的现有(遗留)代码是不安全的。我认为这是非常有力的理由

    • 针对非特定捕获物的指南,重新投掷
    • 集中式异常策略(例如 using a Lippincott function 用于异常处理程序

      注意最后一个想法可能在协程中也被明确禁止:

      ⚠重要

      不要从 catch 块内部跳转,而是在另一个执行上下文中重新抛出异常。

      更新:正如 @DeadMG 刚刚评论那篇文章,我们可以简单地将 Lippincott 函数转换为一个包装函数,它可以在集中异常处理的同时满足 Coroutine 的要求。

    【讨论】:

    • @sehe 你是对的,我的示例代码不正确,我错过了forced_unwind rethrow。但是,请注意,即使您添加了重新抛出,代码也无法按预期工作。我希望“my_timer enter”和“my_timer return”输出,但实际上我只收到“my_timer enter”消息。我敢打赌,你可以在 boost 1.54 和 VM 上看到同样的结果。
    • @NikkiChumakov 是的。我认为还有别的问题。会不会是处理程序的结果类型被推断为无效? (我将auto 替换为void 以在c++11 上编译)。这似乎是 Boost Coroutine 放松的一个原因
    • 我不知道是什么原因,只是想找出来。 :) 我注意到的是,如果我用一些简单的包装类或什至用 lambda 包装 async_wait 处理程序,一切都会按预期工作。 coliru.stacked-crooked.com/a/c498fe675ce4ccde
    • 那将是因为随后为 async_result 特征和 handler_invoke 自定义点选择了不同的特化/ADL 重载。 (老实说,我没有跟上你在这里想要达到的目标。我有点假设你知道:))
    • 直到今天我才看到这个问题,但要点是 Boost.Asio 如果知道没有可用于恢复它的处理程序,它会阻止协程无限期挂起。发生这种情况时,Boost.Asio 将销毁协程,导致挂起的堆栈展开。
    猜你喜欢
    • 2015-03-14
    • 1970-01-01
    • 2020-06-12
    • 2021-09-03
    • 2011-09-21
    • 2015-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多