If I have a class running its own thread, I almost always try to write it like the following, with the thread started by the constructor. The reason for this is that an object can never be constructed on multiple threads, so there is never a danger of two threads trying to start the thread. This class prints a message every second until asked to stop, for example:
#include <mutex>
#include <thread>
#include <condition_variable>
#include <iostream>
#include <chrono>
#include <string>
class a_thread
{
public:
explicit a_thread(const std::string& msg)
: _msg(msg)
, _stop(false)
, _t(&a_thread::thread_main, this)
{
}
~a_thread()
{
stop();
}
void stop()
{
std::unique_lock<std::mutex> l(_m);
if (!_stop)
{
_stop = true;
l.unlock();
_cv.notify_all();
if (_t.joinable())
_t.join();
}
}
private:
// Disable copying (and moving)
a_thread(const a_thread&) = delete;
a_thread& operator=(const a_thread&) = delete;
void thread_main()
{
using namespace std::chrono;
auto next_print_time = steady_clock::now();
std::unique_lock<std::mutex> l(_m);
while (!_stop)
{
const auto now = steady_clock::now();
if (now >= next_print_time)
{
std::cout << _msg << '\n';
next_print_time = now + seconds(1);
}
else
{
_cv.wait_until(l, next_print_time);
}
}
}
std::string _msg;
bool _stop;
std::mutex _m;
std::condition_variable _cv;
std::thread _t;
};
However, sometimes it is desirable to defer the starting of the thread, which I implement like so:
#include <mutex>
#include <thread>
#include <condition_variable>
#include <iostream>
#include <chrono>
#include <string>
class b_thread
{
public:
explicit b_thread(const std::string& msg)
: _msg(msg)
, _stop(true)
{
}
~b_thread()
{
stop();
}
void start()
{
std::unique_lock<std::mutex> l(_m);
if (_stop)
{
_stop = false;
l.unlock();
_t = std::thread(&b_thread::thread_main, this);
}
}
void stop()
{
std::unique_lock<std::mutex> l(_m);
if (!_stop)
{
_stop = true;
l.unlock();
_cv.notify_all();
if (_t.joinable())
_t.join();
}
}
private:
// Disable copying (and moving)
b_thread(const b_thread&) = delete;
b_thread& operator=(const b_thread&) = delete;
void thread_main()
{
using namespace std::chrono;
auto next_print_time = steady_clock::now();
std::unique_lock<std::mutex> l(_m);
while (!_stop)
{
const auto now = steady_clock::now();
if (now >= next_print_time)
{
std::cout << _msg << '\n';
next_print_time = now + seconds(1);
}
else
{
_cv.wait_until(l, next_print_time);
}
}
}
std::string _msg;
bool _stop;
std::mutex _m;
std::condition_variable _cv;
std::thread _t;
};
Until today, I was confident that this code was thread-safe, in the sense that the stop()
and start()
functions could be safely called from multiple threads on the same instance of the class. However, I think the following code could result in a thread being destroyed without first being joined:
int main()
{
b_thread b("Hello world!");
// Let the main thread be called 'Z'
auto x = std::thread([&b] () { b.start(); }); // Thread X
auto y = std::thread([&b] () { b.stop(); }); // Thread Y
b.start();
x.join();
y.join();
}
If the timing is right then I believe the program could run as follows:
- Thread X has assigned to
_t
, but_t
has not yet acquired the mutex inthread_main()
._stop == false
at the time X released mutex_m
. - Thread Y saw
_stop == false
after acquiring the mutex and therefore it sets_stop = true
before releasing the mutex. It is now calling_cv.notify_all()
. - Thread Z is able to acquire the mutex in this state, and sees
_stop == true
. It assigns to_t
, and therefore destroys the thread constructed by X (which hasn't beenjoin()
ed yet). Badness ensues.
I also often use an atomic bool when my thread loop has a non-interruptible wait in it (for example, a loop calling poll()
), like so. I believe it exhibits the same bug:
void start()
{
if (_stop.exchange(false))
_t = std::thread(&b_thread::thread_main, this);
}
void stop()
{
if (!_stop.exchange(true) && _t.joinable())
_t.join();
}
I'd appreciate thoughts on the following:
- The
a_thread
class. This is a pattern I use a lot so I'd like to know if it could be improved. - Am I correct about the bug in
b_thread
? - If so, is there a way to make
b_thread
safe? What about with the atomic bool variant?