I modified your program a bit and it works now. Let's discuss problems first:
Problem 1: using plain bool variable in 2 threads
Here both threads update the variable and it might lead to a race condition, because it is highly dependent which thread comes first and even end up in undefined behaviour. Undefined behaviour especially might occur when write from one thread is interrupted by another. Here Snps brought up links to the following SO answers:
In addition I was searching if write can be interrupted for bool
on x86 platforms and came across this answer:
Problem 2: Caching as compiler optimization
Another problem is that variables are allowed to be cached. It means that the «playing thread» might cache the value of keepRunning
and thus never terminate or terminate after considerable amount of time. In previous C++ version (98, 2003) a volatile
modifier was the only construct to mark variables to prevent/avoid caching optimization and in this case force the compiler to always read the variable from its actual memory location. Thus given the «playing thread» enters the while
loop keepRunning
might be cached and never read or with considerable delays no matter when stop()
modifies it.
After C++ 11 atomic
template and atomic_bool
specialization were introduced to make such variables as non-cachable and being read/set in an uninterruptible manner, thus adressing Problems 1 & 2.
Side note: volatile
and caching explained by Andrei Alexandrescu in the Dr. Dobbs article which addresses exactly this situation:
Caching variables in registers is a very valuable optimization that applies most of the time, so it would be a pity to waste it. C and C++ give you the chance to explicitly disable such caching. If you use the volatile modifier on a variable, the compiler won't cache that variable in registers — each access will hit the actual memory location of that variable.
Problem 3: stop was called before _play()
function was even started
The problem here is that in multi-threaded OSs scheduler grants some time slice for a thread to run. If the thread can progress and this time slice is not over thread continues to run. In «main thread» all play()
calls were executed even before the «play threads» started to run. Thus the object destruction took place before _play()
function started running. And there you set the variable keepRunning
to true
.
How I fixed this problem
We need to ensure that play()
returns when the _play()
function started running. A condition_variable
is of help here. play()
blocks so long until _play()
notifies it that it has started the execution.
Here is the code:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
class AudioPlayer
{
atomic_bool keepRunning;
thread thread_play;
std::mutex mutex;
std::condition_variable play_started;
public:
AudioPlayer()
: keepRunning{false}
{}
~AudioPlayer(){ stop(); }
void play()
{
stop();
std::unique_lock<std::mutex> lock(mutex);
thread_play = thread(&AudioPlayer::_play, this);
play_started.wait(lock);
}
void stop()
{
keepRunning = false;
cout << "stop called" << endl;
if (thread_play.joinable()) thread_play.join();
}
void _play()
{
cout << "Playing: started\n";
keepRunning = true; // B: this causes problem
play_started.notify_one();
while(keepRunning)
{
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "Playing: stopped\n";
}
};
int main()
{
AudioPlayer ap;
ap.play();
ap.play();
ap.play();
return 0;
}