4

I have this race condition with an audio playback class, where every time I start playback I set keepPlaying as true, and false when I stop.

The problem happens when I stop() immediately after I start, and the keepPlaying flag is set to false, then reset to true again.

I could put a delay in stop(), but I don't think that's a very good solution. Should I use conditional variable to make stop() wait until keepPlaying is true?

How would you normally solve this problem?

#include <iostream>
#include <thread>
using namespace std;


class AudioPlayer

{
    bool keepRunning;
    thread thread_play;

    public: 
    AudioPlayer(){ keepRunning = false; }
    ~AudioPlayer(){ stop(); }

    void play()
    {
        stop();
        // keepRunning = true; // A: this works OK
        thread_play = thread(&AudioPlayer::_play, this);
    }
    void stop()
    {
        keepRunning = false;
        if (thread_play.joinable()) thread_play.join();
    }
    void _play()
    {
        cout << "Playing: started\n";
        keepRunning = true; // B: this causes problem
        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;
}

Output:

$ ./test
Playing: started
(pause indefinitely...)

bot1131357
  • 877
  • 8
  • 25
  • 3
    Some people, when confronted with a problem, think, "I know, I'll use threads," and then two they hav erpoblesms. – Slava May 12 '17 at 15:19
  • Thanks for all your insightful answers. didiz's solution to use mutex might be the simplest way to go after all. Thanks Dan Allen for reminding me to reuse the same thread. – bot1131357 May 16 '17 at 11:22

6 Answers6

2

Here is my suggestion, combining many comments from below as well:

1) Briefly synchronized the keepRunning flag with a mutex so that it cannot be modified while a previous thread is still changing state.

2) Changed the flag to atomic_bool, as it is also modified while the mutex is not used.

class AudioPlayer
{
    thread thread_play;

public:
    AudioPlayer(){ }
    ~AudioPlayer()
    {
        keepRunning = false;
        thread_play.join();

    }

    void play()
    {
        unique_lock<mutex> l(_mutex);
        keepRunning = false;
        if ( thread_play.joinable() )
            thread_play.join();
        keepRunning = true;
        thread_play = thread(&AudioPlayer::_play, this);
    }
    void stop()
    {
       unique_lock<mutex> l(_mutex);
       keepRunning = false;
    }
private:
    void _play()
    {
        cout << "Playing: started\n";
        while ( keepRunning == true )
        {
            this_thread::sleep_for(chrono::milliseconds(10));
        }
        cout << "Playing: stopped\n";
    }

    atomic_bool keepRunning { false };
    std::mutex _mutex;
};




int main()
{
    AudioPlayer ap;
    ap.play();
    ap.play();
    ap.play();
    this_thread::sleep_for(chrono::milliseconds(100));
    ap.stop();
    return 0;
}
didiz
  • 1,069
  • 13
  • 26
  • 2
    As a side note, it is very expensive to start a new `thread` for every request. Using `async` for the player thread as well (with `launch::async`) will improve performance. – didiz May 12 '17 at 09:30
  • 1
    "_3) I added an async call so as not to block in stop_" - the `std::future` returned by `std::async` will block in its destructor until the callable returns. _I.e._, this will still block. – Felix Glas May 12 '17 at 10:03
  • thanks you are indeed correct (editing). while MSVC disrespects that behavior it appears that it is the standard. – didiz May 12 '17 at 10:08
  • @didiz MSVC disrespects many parts of the C++ Standard (and probably C too) :( People are trying to help them improve, though, so it'd be useful if you could file a bug with them for this, if one doesn't already exist. – underscore_d May 13 '17 at 08:38
  • It appears the latest release has a fix for this already. Quite limiting for devs writing multi-platform stuff. Thanks for the tip! – didiz May 13 '17 at 08:48
2

To answer the question directly.

Setting keepPlaying=true at point A is synchronous in the main thread but setting it at point B it is asynchronous to the main thread.

Being asynchronous the call to ap.stop() in the main thread (and the one in the destructor) might take place before point B is reached (by the asynchronous thread) so the last thread runs forever.

You should also make keepRunning atomic that will make sure that the value is communicated between the threads correctly. There's no guarantee of when or if the sub-thread will 'see' the value set by the main thread without some synchronization. You could also use a std::mutex.

Other answers don't like .join() in stop(). I would say that's a design decision. You certainly need to make sure the thread has stopped before leaving main()(*) but that could take place in the destructor (as other answers suggest).

As a final note the more conventional design wouldn't keep re-creating the 'play' thread but would wake/sleep a single thread. There's an overhead of creating a thread and the 'classic' model treats this as a producer/consumer pattern.

#include <iostream>
#include <thread>
#include <atomic>

class AudioPlayer

{
    std::atomic<bool> keepRunning;
    std::thread thread_play;

    public: 
    AudioPlayer():keepRunning(false){ 
    }
    ~AudioPlayer(){ stop(); }

    void play()
    {
        stop();
        keepRunning = true; // A: this works OK
        thread_play = std::thread(&AudioPlayer::_play, this);
    }

    void stop()
    {
        keepRunning=false;
        if (thread_play.joinable()){
            thread_play.join(); 
        } 
    }
    void _play()
    {
        std::cout<<"Playing: started\n";
        while(keepRunning)
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); 
        }
        std::cout<<"Playing: stopped\n";
    }
};


int main()
{
    AudioPlayer ap;

    ap.play();
    ap.play();
    ap.play();
    ap.stop();
    return 0;
}

(*) You can also detach() but that's not recommended.

Persixty
  • 8,165
  • 2
  • 13
  • 35
1

Ideally, you'd just set keepPlaying before starting the thread (as in your commented out play() function). That's the neatest solution, and skips the race completely.

If you want to be more fancy, you can also use a condition_variable and signal the playing thread with notify_one or notify_all, and in the loop check wait_until with a duration of 0. If it's not cv_status::timeout then you should stop playing.

Don't make stop pause and wait for state to settle down. That would work here, but is a bad habit to get into for later.

As noted in the comment, it is undefined behavior to write to a variable while simultaneously reading from it. atomic<bool> solves this, but wouldn't fix your race on its own, it just makes the reads and writes well defined.

Donnie
  • 45,732
  • 10
  • 64
  • 86
1

First, what you have here is indeed the definition of a data race - one thread is writing to a non-atomic variable keepRunning and another is reading from it. So even if you uncomment the line in play, you'd still have a data race. To avoid that, make keepRunning a std::atomic<bool>.

Now, the fundamental problem is the lack of symmetry between play and stop - play does the actual work in a spawned thread, while stop does it in the main thread. To make the flow easier to reason about, increase symmetry:

  • set keepRunning in play, or
  • have play wait for the thread to be up and running and done with any setup (also eliminating the need for the if in stop).

As a side note, one way to handle cases where a flag is set and reset in possibly uneven order is to replace it with a counter. You then stall until you see the expected value, and only then apply the change (using CAS).

Eran
  • 21,632
  • 6
  • 56
  • 89
1

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;
}
Community
  • 1
  • 1
ovanes
  • 5,483
  • 2
  • 34
  • 60
  • As I wrote: this was the only way to prevent caching of variable in older C++ standards. I did not state, that `volatile` does synchronization. Please read this article to understand what I was referring to: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766 – ovanes May 12 '17 at 09:51
  • 1
    "_There is no problem with data race at this point._" - Wrong. Nothing is atomic by default in C++, not even `bool`. Partial writes can occur. And reorderings. _E.g._, imagine that `keepRunning = true` is moved below `while(keepRunning)` by the compiler. – Felix Glas May 12 '17 at 10:05
  • @Snps: Did I state that anywhere? That bool is atomic? What happens with `bool` variable when it is modified? It is loaded into register, modified and than stored back into memory. Any of these operations are uninterruptible. You can't intterupt `write` into register, only between load into register and write! There is also no way to write half-true or half-false into the bool. It is either written or not. The problem is that such var can be cached and not update in the other thread! – ovanes May 12 '17 at 10:10
  • 1
    @ovanes Watch out with that article - lots of wrong things. – BЈовић May 12 '17 at 10:11
  • @Snps: I understand Pete's answer, but he address all kind of architecture and probably embedded ones. I assume here that op was figuring it out for x86 and can refer to this answer as well: http://stackoverflow.com/a/14624990/98693 – ovanes May 12 '17 at 10:20
  • @BЈовић: What else is wrong here? Where are other "lots of wrong things" – ovanes May 12 '17 at 10:22
  • 1
    @ovanes Yes, on some architectures `bool` will be "_updated in one go_", but nothing in OPs question indicates that he specifically is targeting, _e.g._, x86. The point is that the **C++ standard** says that _nothing_ is atomic by default. – Felix Glas May 12 '17 at 10:24
  • @Snps: But I did not refer to the standard and where did I state that bool is updated in one go I also take in my answer interruption in account. Let's just analyze what happens if bool is partially written? No matter what it was before it's bits will be either all 0 or some of them 1. Would C++ §4.12 apply here? If yes we clearly get either `true` or `false`. As I wrote the only caveat would be that the thread does some more cycles until the value is fully written out. Not so? – ovanes May 12 '17 at 10:42
  • 1
    @ovanes There's more to it than §4.12. On some architectures there's this thing called _trap representations_ (http://stackoverflow.com/questions/6725809/trap-representation). _I.e._, to C++, reading a partial write is always considered UB and anything could happen. – Felix Glas May 12 '17 at 10:46
  • 1
    @ovanes You specifically stated that there initially is no problem with a data race in OPs code, which is wrong. A data race can occur when reading & writing to the same memory concurrently, even if it's a `bool`, and this is UB. This is the main problem of OPs code. – Felix Glas May 12 '17 at 10:48
  • Ok, I will rephrase this. – ovanes May 12 '17 at 10:51
  • @Snps: changed the answer to address all issues discussed. – ovanes May 12 '17 at 11:01
  • @ovanes As I said, your answer tells to use volatile to fix concurrent access to that variable, and that is wrong. The linked article tells the same thing, therefore it is wrong. – BЈовић May 12 '17 at 12:05
  • 1
    @BЈовић: Mhh... Very strange understanding of my answer! Problem 1 addresses all kind of issues of writing to same variable from different threads. Problem 2 explains caching problems and how these problems were fixed with C++98, 2003 and new versions C++11 onwards. Then I present the fix, where I don't use `volatile` at all. Why do you conclude that I fix concurrent access with volatile? What exactly make you think so? – ovanes May 12 '17 at 12:14
0

Your solution A is actually almost correct. It's still undefined behavior to have one thread read from non-atomic variable that another is writing to. So keepRunning must be made an atomic<bool>. Once you do that and in conjunction with your fix from A, your code will be fine. That is because stop now has a correct post condition that no thread will be active (in particular no _play call) after it exits.

Note that no mutex is necessary. However, play and stop are not themselves thread safe. As long as the client of AudioPlayer is not using the same instance of AudioPlayer in multiple threads though that shouldn't matter.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72