Reputation: 1711
Sorry for the title, I couldn't find a one that describes the problem clearly.
Please read the snippet below. There are 2 threads where the first one controls the execution and the second one does the job.
std::atomic<bool> m_running;
Process* process; // A thread-safe process implementation
//thread 1
void stop()
{
m_running = false;
if( process->isRunning() )
process->kill(); // process->join() returns as a result of this call
}
//thread 2
void run()
{
m_running = true;
while( m_running )
{
do_something();
process->start();
process->join(); // Blocking call
}
m_running = false;
}
I've noticed that if the first thread starts execution while the second thread is busy with the do_something()
, it will assign false
to m_running
and check if the process is running or not. Since the second thread is still in the do_something()
call, process->isRunning()
will return false
and the first thread will return without killing the process.
But second thread will start the process as soon as finishing do_something()
as opposed to our stop demand.
The first thing that come to my mind is like the following :
//thread 2
void run()
{
m_running = true;
while( m_running )
{
do_something();
if( m_running )
{
// flagged line ...
process->start();
}
process->join(); // Blocking call
}
m_running = false;
}
But still it is possible that the whole stop()
method in thread 1 can start and finish when the second thread is interrupted in flagged line.
So I decided to use a mutex but I'm not sure if everything is OK or not :
//thread 1
void stop()
{
mutex.lock();
m_running = false;
if( process->isRunning() )
process->kill(); // process->join() returns as a result of this call
mutex.unlock();
}
//thread 2
void run()
{
m_running = true;
while( m_running )
{
do_something();
mutex.lock();
if( m_running )
{
process->start();
}
mutex.unlock();
process->join(); // Blocking call
}
m_running = false;
}
Upvotes: 3
Views: 2886
Reputation: 21576
Think of it in terms of indivisible transactions.
//thread 1
void stop()
{
m_running = false;
{
//This block should happen as a transaction
std::lock_guard<std::mutex> lck(m_mutex);
if( process->isRunning() )
process->kill();
}
}
//thread 2
void run()
{
m_running = true;
while( m_running )
{
do_something();
{
//Since starting this process depends on m_running
//which could change anytime since we tested it last,
//we test it again, and start, this time as another transaction
std::lock_guard<std::mutex> lck(m_mutex);
if(m_running)
process->start();
}
process->join(); // Blocking call
}
}
You don't need to bother about protecting m_running
since its the default std::atomic<bool>
which is sequentially consistent
Upvotes: 1
Reputation: 118292
Although a more precise answer needs more details on what isRunning()
, kill()
, and join()
, there's a design flaw that's apparent here.
Namely, the m_running
flag's purpose is overloaded.
A flag like m_running
typically implements one of two classical design patterns:
or:
The shown code is attempting to conflate the two design patterns together. And they don't mesh.
If the purpose of m_running
is to have the thread in question report its current status, then thread 1's stop()
has no business setting this flag. m_running
should only be updated by the thread in question, and, indeed, your thread's run()
sets it to true
when it begins, and false
before it returns. But then, run()
itself has no business checking the value of m_running
inside the if
statement. Its only purpose is to report the status of the thread. Not to have the thread do anything.
With this design pattern, kill()
typically implements some means for terminating the thread's execution, usually by sending a message of some kind to the thread, that tells the thread to wind down. kill()
would then monitor and wait until the thread obeys, and sets m_running
to false, indicating that it exited. Or, if this is a joinable thread, kill()
would join the thread.
Alternatively, m_running
's purpose can be to signal the thread to stop what it's doing. But in this situation, the execution thread's run()
has no business updating m_running
itself. All a thread typically does in this design pattern, is execute a while(m_running)
loop, waiting for the next message. The typical process for stopping the thread involves setting m_running
, and then sending some kind of no-op message to the thread -- the message's only purpose would be to wake up the thread, so it can cycle through the loop and see the updated value of m_running
.
The shown code is attempting to conflate the two design patterns into a single variable, and it looks to me like that is the fundamental cause of the design problem.
Separate m_running
into two discrete variables: one to report the state of the thread (if that's needed), and a second variable that's used as part of a reliable mechanism for an orderly shutdown of the thread.
Upvotes: 1