Can a boost::asio::yield_context be used as a dead

2019-02-21 04:36发布

问题:

I'd like to be able to do an asynchronous wait on a specific event. There are a lot of similar questions and answers on here (and all compile and work for me) but none with my specific scenario. Basically, what I need to be able to do is an async_wait, passing a yield context as the handler, to a timer that waits indefinitely, and is then canceled by another thread.

For example, there is this question which does something very similar, but instead of using a yield context, it uses a separate, standalone handler. There is also something like this question which uses a yield context, but waits for a specified amount of time.

I can change my code to look like either of the two examples above and things work fine. But for someone reason when I combine a yield_context handler and a cancelled timer, I get the following exception:

libc++abi.dylib: terminating with uncaught exception of type boost::exception_detail::clone_impl<boost::exception_detail::current_exception_std_exception_wrapper<std::runtime_error> >: 
Program ended with exit code: 9

From what I can tell, it looks like things choke when trying to invoke the completion handler (which in this case is the yield context).

Alright, enough babbling, here's the code. I've tried to come up with as simple of an example as possible to illustrate it:

The class:

class Foo {
public:
  Foo() : work_(io_service_), timer_(io_service_) {
    thread_pool_.create_thread(boost::bind(&boost::asio::io_service::run, &io_service_));
    timer_.expires_from_now(boost::posix_time::pos_infin);
  }
  ~Foo() {
    io_service_.stop();
    thread_pool_.join_all();
  }

  void Wait(const boost::asio::yield_context& context) {
    std::cout << "Waiting" << std::endl;
    timer_.async_wait(context);
    std::cout << "Done waiting" << std::endl;
  }

  void Notify() {
    std::cout << "Notifying" << std::endl;
    timer_.cancel();
  }

  void Write(int num) {
    std::cout << "Sending buffer event" << std::endl;
    Notify();
    std::cout << "Sent buffer event" << std::endl;
  }

  void Read(const boost::asio::yield_context& context) {
    std::cout << "Waiting on buffer event, size is " << buffer_.size() << std::endl;
    Wait(context);
    std::cout << "Received buffer event, size is now " << buffer_.size() << std::endl;
  }

  std::vector<int> buffer_;
  boost::asio::io_service io_service_;
  boost::thread_group thread_pool_;
  boost::asio::io_service::work work_;
  boost::asio::deadline_timer timer_;
};

Main:

boost::shared_ptr<Foo> foo(new Foo());    
boost::asio::spawn(foo->io_service_, boost::bind(&Foo::Read, foo, _1));
boost::this_thread::sleep(boost::posix_time::seconds(2));
foo->Write(1);
boost::this_thread::sleep(boost::posix_time::seconds(4));

Output:

Waiting on buffer event
Waiting
Sending buffer event
Notifying
Sent buffer event
libc++abi.dylib: terminating with uncaught exception of type boost::exception_detail::clone_impl<boost::exception_detail::current_exception_std_exception_wrapper<std::runtime_error> >: 

Now, if I change the wait method to a time that will time out before the cancel is called, everything is fine. I.e.:

void Wait(const boost::asio::yield_context& context) {
    std::cout << "Waiting" << std::endl;
    timer_.expires_from_now(boost::posix_time::seconds(1));
    timer_.async_wait(context);
    std::cout << "Done waiting" << std::endl;
  }

Or, if I change wait to use a separate handler method, everything is fine. I.e.:

void Handler() {
  std::cout << "Handler!" << std::endl;
}

void Wait(const boost::asio::yield_context& context) {       
  std::cout << "Waiting" << std::endl;
  timer_.async_wait(boost::bind(&Foo::Handler, this));
  std::cout << "Done waiting" << std::endl;
}

I'm assuming there must be something simpler I'm missing here: either this is impossible for some reason or I'm making some dumb mistake. Anyway, thanks in advance.

回答1:

The async_wait() operation is being cancelled, resulting in the asynchronous operation failing with an error code of boost::asio::error::operation_aborted. As noted in the Stackful Coroutines documentation, when the boost::asio::yield_context detects that the asynchronous operation has failed, it converts the boost::system::error_code into a system_error exception and throws. Within the coroutine, consider either:

  • Initiating the asynchronous operation with a handler of context[error_code], causing the yield_context to populate the provided boost::system::error_code on failure rather than throwing.

    boost::system::error_code error;
    timer_.async_wait(context[error]); // On failure, populate error.
    
  • Catch the system_error and suppress it.


On failure Boost.Asio will populate a boost::system::error_code if the application is capable of receiving it, otherwise it will throw an exception. This pattern can be observed throughout Boost.Asio:

  • All asynchronous operation handler's accept an lvalue const boost::system::error_code as their first parameter. Hence, the initiating function should not throw, as the application will be informed of the error within the handler. This is not always apparent when using functors that discards extra arguments, such as boost::bind.
  • Synchronous operations are overloaded to support throwing and non-throwing versions. For example, timer.cancel() will throw on failure, where as timer.cancel(boost::system::error_code&) will set the error_code to indicate the error.
  • As noted above, when an asynchronous operation fails within a stackful coroutine and the yield_context handler is not provided a boost::system::error_code, then a system_error exception will be thrown.
  • When using futures, if the asynchronous operation fails, then the error_code is converted into a system_error exception and passed back to the caller through the future.

Here is a complete minimal example based on the original problem that runs to completion.

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

int main()
{
  boost::asio::io_service io_service;
  boost::asio::deadline_timer timer(io_service);
  timer.expires_from_now(boost::posix_time::pos_infin);

  boost::asio::spawn(io_service,
    [&](boost::asio::yield_context yield)
    {
      // As only one thread is processing the io_service, the posted
      // timer cancel will only be invoked once the coroutine yields.
      io_service.post([&](){ timer.cancel(); });

      // Initiate an asynchronous operation, suspending the current coroutine,
      // and allowing the io_service to process other work (i.e. cancel the 
      // timer).  When the timer is cancelled, the asynchronous operation is
      // completed with an error,  causing the coroutine to resume.  As an
      // error_code is provided, the operation will not throw on failure.
      boost::system::error_code error;
      timer.async_wait(yield[error]);
      assert(error == boost::asio::error::operation_aborted);
    });

  io_service.run();
}