I have a Base class which acts as an interface to multiple strategies for synchronous event processing. I now want the strategies to process the events asynchronously. To minimize code refactor, each strategies will have its own internal thread for asynchronous event processing. My main concern is how to manage the lifecycle of this thread. The Derived strategies classes are constructed and destructed all around the codebase so it would be hard to manage the thread lifecycle (start/stop) outside of the strategies classes.
I ended up with the following code:
#include <iostream>
#include <cassert>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
struct Base
{
virtual ~Base()
{
std::cout << "In ~Base()" << std::endl;
// For testing purpose: spend some time in Base dtor
boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
}
virtual void processEvents() = 0;
void startThread()
{
if(_thread)
{
stopThread();
}
_thread.reset(new boost::thread(&Base::processEvents, this));
assert(_thread);
}
void stopThread()
{
if(_thread)
{
std::cout << "Interrupting and joining thread" << std::endl;
_thread->interrupt();
_thread->join();
_thread.reset();
}
}
boost::shared_ptr<boost::thread> _thread;
};
struct Derived : public Base
{
Derived()
{
startThread();
}
virtual ~Derived()
{
std::cout << "In ~Derived()" << std::endl;
// For testing purpose: make sure the virtual method is called while in dtor
boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
stopThread();
}
virtual void processEvents()
{
try
{
// Process events in Derived specific way
while(true)
{
// Emulated interruption point for testing purpose
boost::this_thread::sleep(boost::posix_time::milliseconds(100));
std::cout << "Processing events..." << std::endl;
}
}
catch (boost::thread_interrupted& e)
{
std::cout << "Thread interrupted" << std::endl;
}
}
};
int main(int argc, char** argv)
{
Base* b = new Derived;
delete b;
return 0;
}
As you can see, the thread is interrupted and joined in the Derived class destructor. Many comments on Stackoverflow argues that it’s a bad idea to join a thread in a destructor. However, I can’t find a better idea considering the constraint that the thread lifecycle must be managed through the construction/destruction of the Derived class. Does someone has a better proposition?
It is a good idea to release resources a class creates when the class is destroyed, even if one of the resources is a thread. However, when performing any non-trivial task in a destructor, it is often worth taking the time to examine the implications in full.
Destructors
A general rule is to not throw exceptions in destructors. If a
Derivedobject is on a stack that is unwinding from another exception, andDerived::~Derived()throws an exception, thenstd::terminate()will be invoked, killing the application. WhileDerived::~Derived()is not explicitly throwing an exception, it is important to consider that some of the functions it is invoking may throw, such as_thread->join().If
std::terminate()is the desired behavior, then no change is required. However, ifstd::terminate()is not desired, then catchboost::thread_interruptedand suppress it.Inheritance
It looks as though inheritance was used to for code reuse and minimizing code refactoring by isolating the asynchronous behavior to be internal to the
Basehierarchy. However, some of the boilerplate logic is also inDervied. As classes derived fromBaseare already having to be changed, I would suggest considering aggregation or the CRTP to minimize the amount of boilerplate logic and code within these classes.For example, a helper type can be introduced to encapsulate the threading logic:
This helper class could be aggregated and initialized in
Derived‘s constructor. It removes the need for much of the boilerplate code, and can be reused elsewhere:Another key point is that the
AsyncJobforces a Boost.Thread interruption point into the loop logic. The job shutdown logic is implemented in terms of interruption points. Thus, it is critical that an interruption point be reached during iterations. Otherwise, it could be possible to end up in a deadlock if the user code never reaches an interruption point.Lifespan
Examine whether it is the thread’s lifetime that must be associated with the object’s lifetime, or if it is the asynchronous event processing that needs to be associated with the object’s lifetime. If it is the latter, then it may be worth considering using thread pools. A thread pool could provide finer grain control over thread resources, such as imposing a maximum limit, as well as minimize the amount of wasted threads, such as threads doing nothing or time spent creating/destroying short-lived threads.
For example, consider the case where a user creates an array of 500
Derviedclasses. Are 500 threads needed to handle 500 strategies? Or could 25 threads handle 500 strategies? Keep in mind that on some systems, thread creation/destruction can be expensive, and there may even be maximum thread limit imposed by the OS.In conclusion, examine the tradeoffs, and determine which behaviors are acceptable. It can be difficult to minimize code refactoring, particularly when changing the threading model that has implications to various areas of the codebase. The perfect solution is very rarely obtainable, so identify the solution that covers the majority of cases. Once the supported behavior has been clearly defined, work on modifying existing code so that it is within the supported behavior.