Search code examples
c++multithreadingboostboost-asioshared-ptr

how to properly shutdown instance of class that uses asio for event queuing


In my project, I am extensively using boost asio to be able to uniformly queue non-uniform events to the modules in my application, using io_service.post() and strand.post()/dispatch().

In the main(), these modules are created and held in shared_ptrs until the program exits and they get deleted:

simplified main:
{
  [some initialization]

  boost::asio::io_service service;

  [create pool of worker threads that drive the io_service]

  {
    boost::shared_ptr<manager_a> a(new manager_a(service, foo));
    boost::shared_ptr<manager_b> b(new manager_b(service, bar, blah));
    ...

    [wait for signal to shutdown (SIGINT (release), cin.getc() (debug), or similar)]
  }

  [some shutdown (join thread pool)]
}

The "managers" (I don't know what to call them) derive from boost::enable_shared_from_this. In their constructors, they may register callbacks with other modules:

manager_a::manager_a(boost::asio::io_service& service, module *m) :
  m_service(service),
  m_strand(service),
  m_module(m)
{
  // manager_a implements some callback interface
  // that "module" may call from another thread
  m_module->register(this);
}

In the destructor:

manager_a::~manager_a()
{
  m_module->unregister(this);
}

The manager implements the callback interface by posting a call through the strand:

void manager_a::on_module_cb(module_message m)
{
  // unsafe to do work here, because the callback is from an alien thread
  m_strand.dispatch(boost::bind(&manager_a::handle_module_message, shared_from_this(), m));
}
void manager_a::handle_module_message(module_message m)
{
  // safe to do work here, as we're serialized by the strand
}

Now the dilemma I'm in:

If "module" calls the callback immediately after the register(this) in the constructor, the shared_ptr in the main has not taken over the instance yet, and shared_from_this() in the callback function throws an exception. Same problem with the destructor - the shared_ptr has determined it had the last reference and when the destructor is called, if the callback gets called before the unregister(this), shared_from_this() throws.

The reason why shared_from_this() is needed is because queued function calls are stored in the io_service that hold a pointer to the manager instance, and that are independent from the manager's lifetime. I can give a raw this to the "module" because I can unregister with it before manager_a is destroyed, but the same is not true for queued function calls in the io_service, so a shared_from_this() is needed to keep the instance alive as long as there's some message posted. You can't cancel those either, and you can't blocking-wait in the destructor until all pending posts are delivered. Unfortunately as it is now, I can't even prevent new posts from being (tried to be) queued during the critical constructor/destructor phases.

Some ideas:

  1. Write start/stop functions and do register/unregister there. E.g. create a factory function like this:

    boost::shared_ptr<manager_a> manager_a::create(io_service& s, module *m)
    {
      boost::shared_ptr<manager_a> p(new manager_a(s, m));
      p->start();
      return p;
    }
    

    This only works for the creation, not for destruction. Giving a custom deleter to the shared_ptr, that calls stop() before delete, doesn't help, as it is too late anyhow.

  2. Have the main() call start() and stop(), but I don't see how main() could do this in an exception-safe way for the stop() call, i.e. be sure to call stop() even if some later code throws. Having another RAII class just to call stop() seems awkward.

  3. Just wrap the strand.dispatch() call in a try/catch and ignore the exception. This is an indication that destruction is in progress (or construction not finished), so just ignore the callback. This feels hackish, and I'd rather not do that. If I had access to the embedded weak_ptr in the enable_shared_from_this base class, I could call the non-throwing lock() and check the return value to see if it is valid. But shared_from_this does not give you access, and it still would seem hackish anyway... Besides, missing a callback during construction is not helpful, even If I could work around that.

  4. Let manager_a have it's own io_service and a thread driving it. Then, in the destructor, I can stop the service and join the thread and be sure there are no pending posts in the io_service. There's also no need for shared_from_this() anymore, or for a strand. This would work, I guess, but then, having a thread pool in the main() for all managers becomes pointless, and I'd have much more threads in my application than seems sensible. There are enough threads already in other modules that don't use an asio io_service...


Solution

  • It's not clear from your question, but if all your manager objects are created at start-up and deleted on exit, then the solution to your problem is to create the managers before you start the IO threads, and stop the IO threads before you delete the managers. That will stop you getting callbacks when you don't want them.