6

Is there anything wrong in following source code if I don't use mutex?

bool bStop = false;

void thread1_fun()
{
    while (!bStop)
    {
        doSomething();
    }
}

void thread2_fun()
{
    bStop = true;
}
tillman
  • 139
  • 2
  • 9
  • You have to declare `bStop` as `volatile`, otherwise you are subject to processor restrictions. –  Dec 18 '17 at 11:45
  • Nothing will guarantee that `thread1_fun` actually fetches the value of `bStop` each iteration. – Jonathon Reinhart Dec 18 '17 at 11:46
  • it can work sometimes, sometimes it will not, as was said, bStop should be volatile, else you get unexpected behaviour – Angen Dec 18 '17 at 11:46
  • 5
    There are no guarantees that the changes in one thread are visible from other threads. For a single variable you could use an `atomic` instead of a mutex. – Bo Persson Dec 18 '17 at 11:47
  • 1
    `bool` is not a magical thread-safe type. – molbdnilo Dec 18 '17 at 11:47
  • 15
    @MarkYisri: `volatile` has **nothing** to do with threads in C++ (although some ill-advised vendor decided to make it behave like atomics). – Dietmar Kühl Dec 18 '17 at 11:47
  • Related: https://stackoverflow.com/questions/222916/in-a-multi-threaded-c-app-do-i-need-a-mutex-to-protect-a-simple-boolean – Jonathon Reinhart Dec 18 '17 at 11:47
  • 5
    @JonathonReinhart, I am just seeing the question you linked as related for the first time here on SO; And I ended up downvoting the answers. Most of the answers there are completely **wrong** today. – WhiZTiM Dec 18 '17 at 12:18

3 Answers3

16

It is undefined behaviour to write to an object in one thread while another thread accesses the object at all.

Unless you specifically inform the compiler that there should be a fence, such as the use of std::atomic, std::mutex et al, all bets are off.

The compiler is within its rights to re-write the code as this:

bool bStop = false;

void thread1_fun()
{
    const bool stopped = bStop;
    // compiler reasons: "because there is no fence, the variable clearly cannot
    // have changed to true, since no-other thread will modify it, since
    // to modify it without a fence would be UB." 
    while (!stopped)  
    {  
        doSomething();
    }
}

void thread2_fun()
{
    // this happens in my thread's view of the world, 
    // but no other thread need see it.
    bStop = true;  
} 
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
9

Is there anything wrong in following source code if I don't use mutex?

Yes, you have a race condition, which leads to undefined behavior.

You need to either use std::atomic<bool> or protect the writes/reads to bStop with a synchronisation primitive such as std::mutex.


Contrary to (sadly) common belief, marking bStop as volatile does not solve anything here. The volatile qualifier has nothing to do with thread-safety.

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 1
    Great of you to make a point about `volatile`, however it may *seem* to have an effect on thread safety because it disallows the compiler to optimize away an access. I found something like this once in a codebase where the devs had a condition they were waiting on in a `std::condition_variable`, and they had found that adding `volatile` fixed their issue of the cv waiting infinitely. (ofc I removed the qualifier and made the vbl being tested `atomic` instead) – AndyG Dec 18 '17 at 12:23
  • @AndyG, that's strange, It smells like a (compiler maybe) bug somewhere. But it would be really interesting to know how and why `volatile` solved the problem. – WhiZTiM Dec 18 '17 at 12:31
  • @WhiZTiM: Good point, it *was* in the early days of Microsoft's support of C++11. I don't remember all the details exactly but the mechanism by which the condition variable returned `true` or `false` was a little convoluted. – AndyG Dec 18 '17 at 12:36
  • 1
    @WhiZTiM `volatile` makes the compiler treat all reads and writes to the qualified variable as if they had side-effects. It generally avoids some optimizations and may make "work" undefined behaviours. (and by "work", I mean "fail indistinguishably with expected behaviour") – YSC Dec 18 '17 at 13:11
2

Writing to a variable in one thread and reading the value of that variable in another thread is a data race. A program with a data race has undefined behavior; that means that the C++ standard doesn't tell you what that program will do. You can avoid the data race by making the variable atomic or by using various kinds of synchronization, the most common being std::mutex and std::condition_variable.

You might be able to get a program to "work" reliably without synchronization by analyzing the details of the code that you're compiling, the details of how your compiler generates object code from that source code, and the details of how your hardware treats the resulting object code. And then, if you change any of those three, you have to do the analysis all over again, because with those changes it might not "work" any more. Of course, if you do that, you're down and dirty with all the kinds of details that high-level languages save you from. Except in very rare circumstances, this simply isn't worth the effort. Use the language correctly.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • The data race in the OP's example is not a problem, and making the variable `atomic` will not eliminate the data race. What making the variable `atomic` will do is, it will force the compiler to emit code that looks at the variable every time around the loop, and it will force the hardware to fetch the value all the way from main memory. – Solomon Slow Dec 18 '17 at 16:13
  • @jameslarge -- your assertion that the data race "is not a problem" is exactly the kind of (non)reasoning that my second paragraph addresses. Do you have any authority for the truth of your assertion? – Pete Becker Dec 18 '17 at 18:56
  • I made an assumption: I saw nothing in the example that controls how many times or, when `doSomething()` is called. So, I assumed that it was not important to coordinate the _last_ call with any other activity. I've seen the same pattern many, many times: A "background" thread performs some task, over and over again until another thread sets a flag that means "stop." Once it's time to stop, it will _always_ be time to stop: Nobody will ever set the flag back to `false`. And in the pattern I'm familiar with, It doesn't matter whether the loop stops "this" time around or "next" time. – Solomon Slow Dec 18 '17 at 20:57
  • @jameslarge -- without synchronization there is no guarantee that the loop will **ever** see the changed value. Sure, many people assume that this will work, and write code accordingly; and maybe it "works" in that when they tried it it did what they expected.That doesn't mean that it will always work. And since making `bStop` atomic is simple and **guarantees** that the change will be propagated to the loop, there's no reason not to do it. – Pete Becker Dec 18 '17 at 21:58
  • Sure, but that's a different issue from the data race. – Solomon Slow Dec 18 '17 at 22:26
  • 1
    @jameslarge -- no, that's exactly what a data race is all about: the behavior of the program **is undefined**. Speculating about what **might** happen can be a fun parlor game, but sound engineering demands that you **know**, and that's what synchronization gets you. – Pete Becker Dec 18 '17 at 23:02
  • We're arguing about different things. You say the OP's example invokes formally undefined behavior (UB). I agree. You say that a program with UB can never be trusted. I agree. You say the OP's example has a data race. Yup, me too. But, I was arguing about what the words, "data race" actually mean. You said, "you can avoid the data race by making the variable atomic." I don't think so: Making it atomic does not eliminate the data race. It only guarantees that the reader thread will see updates by the writer, and not see any bogus values. The "race" still is there. – Solomon Slow Dec 22 '17 at 13:24
  • @jameslarge -- "The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, **at least one of which is not atomic, and neither happens before the other** ..." [intro.multithread]/23 (in C++14) ) (emphasis added) – Pete Becker Dec 22 '17 at 14:21