What's the best way to shut down a Boost thread managed by a C++ class when it's time for an object of that class to be destroyed? I have a class which creates and starts a thread on construction and provides a public Wake()
method which wakes the thread when it's time to do some work. The Wake()
method uses a Boost mutex and a Boost condition variable to signal the thread; the thread procedure waits on the condition variable, then does the work and goes back to waiting.
At the moment, I shut this thread down in the class's destructor, using a boolean member variable as a "running" flag; I clear the flag and then call notify_one() on the condition variable. The thread procedure then wakes up, notices that "running" is false, and returns. Here's the code:
class Worker
{
public:
Worker();
~Worker();
void Wake();
private:
Worker(Worker const& rhs); // prevent copying
Worker& operator=(Worker const& rhs); // prevent assignment
void ThreadProc();
bool m_Running;
boost::mutex m_Mutex;
boost::condition_variable m_Condition;
boost::scoped_ptr<boost::thread> m_pThread;
};
Worker::Worker()
: m_Running(true)
, m_Mutex()
, m_Condition()
, m_pThread()
{
m_pThread.reset(new boost::thread(boost::bind(&Worker::ThreadProc, this)));
}
Worker::~Worker()
{
m_Running = false;
m_Condition.notify_one();
m_pThread->join();
}
void Worker::Wake()
{
boost::lock_guard<boost::mutex> lock(m_Mutex);
m_Condition.notify_one();
}
void Worker::ThreadProc()
{
for (;;)
{
boost::unique_lock<boost::mutex> lock(m_Mutex);
m_Condition.wait(lock);
if (! m_Running) break;
// do some work here
}
}
Is it a good idea to shut down the thread in the class's destructor like this, or should I provide a public method which lets the user do this before the object is destroyed, when there's more potential for error handling and/or forcibly destroying the thread if the thread procedure fails to return cleanly or in good time?
Cleaning up my object's mess in its destructor is appealing as it will require less attention to detail from the user (abstraction, hurrah!) but it seems to me that I should only do things in a destructor if I can guarantee to take full responsibility for cleaning things up successfully and thoroughly, and there's a small chance that code outside the class might one day need to know whether or not the thread was shut down cleanly.
Also, is the mechanism I'm using - writing to a member variable in an object on the stack of one thread and reading that variable in another thread - safe and sane?
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. If the resource is created explicitly via a user call, such as
Worker::Start()
, then there should also be an explicit way to release it, such asWorker::Stop()
. It would also be a good idea to either perform cleanup in the destructor in the event that the user does not callWorker::Stop()
and/or provide the user a scoped helper class that implements the RAII-idiom, invokingWorker::Start()
in its constructor andWorker::Stop()
in its destructor. However, if resource allocation is done implicitly, such as in theWorker
constructor, then the release of the resource should also be implicit, leaving the destructor as the prime candidate for this responsibility.Destruction
Lets examine
Worker::~Worker()
. A general rule is to not throw exceptions in destructors. If aWorker
object is on a stack that is unwinding from another exception, andWorker::~Worker()
throws an exception, thenstd::terminate()
will be invoked, killing the application. WhileWorker::~Worker()
is not explicitly throwing an exception, it is important to consider that some of the functions it is invoking may throw:m_Condition.notify_one()
does not throw.m_pThread->join()
could throwboost::thread_interrupted
.If
std::terminate()
is the desired behavior, then no change is required. However, ifstd::terminate()
is not desired, then catchboost::thread_interrupted
and suppress it.Concurrency
Managing threading can be tricky. It is important to define the exact desired behavior of functions like
Worker::Wake()
, as well as understand the behavior of the types that facilitate threading and synchronization. For example,boost::condition_variable::notify_one()
has no effect if no threads are blocked inboost::condition_variable::wait()
. Lets examine the possible concurrent paths forWorker::Wake()
.Below is a crude attempt to diagram concurrency for two scenarios:
<
and>
are used to highlight when one thread is waking up or unblocking another thread. For exampleA > B
indicates that threadA
is unblocking threadB
.Scenario:
Worker::Wake()
invoked whileWorker::ThreadProc()
is blocked onm_Condition
.Result:
Worker::Wake()
returns fairly quickly, andWorker::ThreadProc
runs.Scenario:
Worker::Wake()
invoked whileWorker::ThreadProc()
is not blocked onm_Condition
.Result:
Worker::Wake()
blocked asWorker::ThreadProc
did work, but was a no-op, as it sent a notification tom_Condition
when no one was waiting on it.This is not particularly dangerous for
Worker::Wake()
, but it can cause problems inWorker::~Worker()
. IfWorker::~Worker()
runs whileWorker::ThreadProc
is doing work, thenWorker::~Worker()
may block indefinitely when joining the thread, as the thread may not be waiting onm_Condition
at the point in which it is notified, andWorker::ThreadProc
only checksm_Running
after it is done waiting onm_Condition
.Working Towards a Solution
In this example, lets define the following requirements:
Worker::~Worker()
will not causestd::terminate()
to be invoked.Worker::Wake()
will not block whileWorker::ThreadProc
is doing work.Worker::Wake()
is called whileWorker::ThreadProc
is not doing work, then it will notifyWorker::ThreadProc
to do work.Worker::Wake()
is called whileWorker::ThreadProc
is doing work, then it will notifyWorker::ThreadProc
to perform another iteration of work.Worker::Wake()
whileWorker::ThreadProc
is doing work will result inWorker::ThreadProc
performing a single additional iteration of work.Code:
Note: As a personal preference, I opted to not allocated
boost::thread
on the heap, and as a result, I do not need to manage it viaboost::scoped_ptr
.boost::thread
has a default constructor that will refer to Not-a-Thread, and it is move-assignable.