boost::asio::yield_context:意外forced_unwind异常

boost::asio::yield_context: unexpected forced_unwind exception

本文关键字:forced unwind 异常 意外 context yield boost asio      更新时间:2023-10-16

我正在为 boost::asio 编写我的自定义异步函数,如此处所述。

但是我得到了 boost::coroutines::d etail::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 entern";
        my_timer (timer, yield);
        std::cout << "my_timer returnsn";
      }
      catch (const boost::coroutines::detail::forced_unwind& e)
      { 
        std::cout << "boost::coroutines::detail::forced_unwindn"; 
      }
    }
  );
  io.run ();
}

科里鲁上的相同代码

更新:

该行为存在于:

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)

简而言之,您需要创建一个处理程序的副本,例如将其发布到 io_service 中,然后再尝试获取async_result以保持协程处于活动状态。


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

  • yield_context CompletionToken 维护对协程的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。 例如,以下内容会将完成处理程序1 发布到调用handler副本的io_service中:

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

如此处所示,使用此附加代码,输出将变为:

my_timer enter
my_timer returns

1. 完成处理程序代码可能会清理一下,但是当我回答如何从不同的线程恢复 Boost.Asio 堆栈协程时,我观察到一些编译器选择了错误的asio_handler_invoke钩子。

这是 Boost 协程实现的详细信息。

如此处所述:异常

⚠ 重要

由协程函数执行的代码不得阻止detail::forced_unwind exception的传播。吸收该异常将导致堆栈展开失败。因此,捕获所有异常的任何代码都必须重新throw任何挂起的detail::forced_unwind异常。

因此,明确需要传递此异常。显式编写处理程序代码,如下所示:

住在科里鲁

try {
  std::cout << "my_timer entern";
  my_timer(timer, yield);
  std::cout << "my_timer returnsn";
}
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 类型的未定义行为。

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

  • 针对非特定渔获物的准则,但露的重新投掷除外
  • 集中式异常策略(如将 Lippincott 函数用于异常处理程序

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

    ⚠ 重要

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

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