9

I'm tring to write my custom async function for boost::asio as described here.

However I'm getting boost::coroutines::detail::forced_unwind exception on line with 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 ();
}

Same code on Coliru

UPDATE:

The behavior exists on:

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

and

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)
Community
  • 1
  • 1
Nikki Chumakov
  • 1,215
  • 8
  • 18

2 Answers2

11

In short, you need to create a copy of handler, such as by posting it into the io_service, before attempting to get the async_result in order to keep the coroutine alive.


Boost.Asio prevents a non-resumable coroutine from indefinitely suspending by destroying the coroutine, resulting in the coroutine's stack to unwind. The coroutine object will throw boost::coroutines::detail::forced_unwind during its destruction, causing the suspended stack to unwind. Asio accomplishes this by:

  • The yield_context CompletionToken maintains a weak_ptr to the coroutine.
  • When the specialized handler_type::type handler is constructed, it obtains a shared_ptr for the coroutine via the CompletionToken's weak_ptr. When the handler is passed as the completion handler to asynchronous operations, the handler and its shared_ptr are copied. When the handler is invoked, it resumes the coroutine.
  • When invoking async_result::get(), the specialization will reset the coroutine shared_ptr owned by the handler that was passed to async_result during construction, and then yield the coroutine.

Here is an attempt to illustrate the execution of the code. Paths in | indicate the active stack, : indicates the suspended stack, and arrows are used to indicate transfer of control:

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

To fix this problem, handler needs to be copied and invoked through asio_handler_invoke() when it is time to resume the coroutine. For example, the following will post a completion handler1 into io_service that invokes a copy of 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 ();

As demonstrated here, with this additional code, the output becomes:

my_timer enter
my_timer returns

1. The completion handler code can likely be cleaned up a bit, but as I was answering how to resume a Boost.Asio stackful coroutine from a different thread, I observed some compilers selecting the wrong asio_handler_invoke hook.

Community
  • 1
  • 1
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Thank you for the answer. After few cups of coffee I understood why posted std::bind handler is always called after coroutine destruction, even in multithreaded io_service execution environment :) However, such design seems to me to be over-complicated and error prone to my taste :( – Nikki Chumakov Feb 16 '15 at 10:29
  • I also found an alternative solution: to use a wrapper that holds the copy of the handler and thus prevents the early coroutine destruction. Code: http://coliru.stacked-crooked.com/a/b639954744728c43 What do you think? Is it reasonable? – Nikki Chumakov Feb 16 '15 at 13:13
  • @Nikki Glad to help. The `std::bind` handler is not called after the destruction of the coroutine; instead, it keeps the coroutine alive as it has a copy of `handler` which holds a `shared_ptr` to the coroutine. The desired goal was not clear to me, so the code I posted was focused on demonstrating proper handler invocation and coroutine yielding. The latest code you posted is functionally very different. What is the desired goal? When an asynchronous operation is initiated, do you wish to be able to do additional work in the coroutine _before_ yielding, or should it yield immediately? – Tanner Sansbury Feb 16 '15 at 18:43
4

This is a Boost Coroutine implementation detail.

As documented here: exceptions

⚠ Important

Code executed by coroutine-function must not prevent the propagation of the detail::forced_unwind exception. Absorbing that exception will cause stack unwinding to fail. Thus, any code that catches all exceptions must re-throw any pending detail::forced_unwind exception.

So, you're explicitly required to pass this exception through. Explicitly code the handler like:

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

This particular exception is an implementation detail and must

  • be expected in coroutine context
  • not be swallowed, or RAII semantics will be violated leading to resource leaks and possibly undefined behaviours with your RAII type.

To be fair, this makes it unsafe to "naively" use existing (legacy) code that might not afford this guarantee. I think this is very strong reason for

  • guidelines against non-specific catches except to bare rethrow
  • centralized exception strategies (like using a Lippincott function for exception handlers)

    Beware that last idea might be expressly prohibited in Coroutines too:

    ⚠ Important

    Do not jump from inside a catch block and than re-throw the exception in another execution context.

    Update: As @DeadMG just commented on that article, we can trivially transform the Lippincott function to a wrapping function, which could satisfy the requirements for Coroutine while centralizing exception handling.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • @sehe you are right, my sample code was not correct and I missed forced_unwind rethrow. However, please notice that even with the rethrow added by you the code does not work as expected. I would expect "my_timer enter" AND "my_timer return" output, but actually I got only "my_timer enter" message. I bet that you could see the same with boost 1.54 and VM. – Nikki Chumakov Nov 18 '14 at 22:26
  • @NikkiChumakov Yes. I think that there is something else the matter. Could it be that the result type of the handler is deduced as void? (I replaced `auto` by `void` to compile at c++11). That seems to be a reason for Boost Coroutine to unwind – sehe Nov 18 '14 at 22:38
  • I don't know what is the reason, just tring to find it out. :) What I noticed is if I wrap the async_wait handler with some trivial wrapper class or even with lambda everything works as expected. http://coliru.stacked-crooked.com/a/c498fe675ce4ccde – Nikki Chumakov Nov 18 '14 at 22:56
  • That would be because different specializations/ADL overloads are then selected for the `async_result` trait and `handler_invoke` customization point. (To be honest, I'm not up to speed with what you're trying to achieve here. I was kinda assuming you knew :)) – sehe Nov 18 '14 at 22:59
  • What I want to achieve is similar to http://stackoverflow.com/questions/24497881/boostasiospawn-yield-as-callback I.e. I want to convert yield_context object into callable object and use it legacy code that is written on top of boost::asio. In my sample code I pass handler directly to asio::async_wait and in real code I would pass it to some legacy function that may pass it later to asio::async_wait or may call it directly according to its internal logic. – Nikki Chumakov Nov 18 '14 at 23:14
  • It may be a good cause to phrase your question /that/ way (or post a new one) focusing on the design goals, instead of the incidental implementation mishaps encountered. (I'd figure that people like Tanner Sansbury would be able to shed some light there.) – sehe Nov 18 '14 at 23:36
  • forced_unwind is thrown if the coroutine (yield_context) is deconstructed (used for stack unwinding). find out why yield_context is destroyed too early. – olk Nov 20 '14 at 08:31
  • 1
    I did not see this question until today, but the gist is that Boost.Asio prevents a coroutine from staying indefinitely suspended if it knows that there are no handlers available to resume it. When this occurs, Boost.Asio will destroy the coroutine, causing the suspended stack to unwind. – Tanner Sansbury Feb 16 '15 at 00:37