3

I've read multiple answers and articles stating why volatile doesn't make multithreaded C++ code safe.

I understand the reasoning, I think understand the possible dangers, my issue is I can't create or find any example code or mention of a situation where a program using it for synchronization produces actually visible wrong or unexpected behavior. I don't even need it to be reproducible (as current compilers even with optimizations seem to try producing safe code), just an example where it really happened.

Community
  • 1
  • 1
Adrian17
  • 433
  • 2
  • 6
  • 33

2 Answers2

5

Say you have a counter that you want to use to keep track of how many times some operation is completed, incrementing the counter each time.

If you run this operation in multiple threads then unless the counter is std::atomic or protected by a lock then you will get unexpected results, volatile will not help.

Here is a simplified example that reproduces the unpredictable results, at least for me:

#include <future>
#include <iostream>
#include <atomic>

volatile int counter{0};
//std::atomic<int> counter{0};

int main() {
    auto task = []{ 
                      for(int i = 0; i != 1'000'000; ++i) {
                          // do some operation...
                          ++counter;
                      }
                  };
    auto future1 = std::async(std::launch::async, task);
    auto future2 = std::async(std::launch::async, task);
    future1.get();
    future2.get();
    std::cout << counter << "\n";
}

Live demo.

Here we are starting two tasks using std::async using the std::launch::async launch policy to force it to launch asynchronously. Each task simply increments the counter a million times. After the two tasks are complete we expect the counter to be 2 million.

However, an increment is a read and write operation between reading the counter and writing to it another thread may have also written to it and increments may be lost. In theory, because we have entered the realm of undefined behaviour, absolutely anything could happen!

If we change the counter to std::atomic<int> we get the behaviour we expect.

Also, say another thread is using counter to detect if the operation has been completed. Unfortunately, there is nothing stopping the compiler from reordering the code and incrementing the counter before it has done the operation. Again, this is solved by using std::atomic<int> or setting up the necessary memory fences.

See Effective Modern C++ by Scott Meyers for more information.

Chris Drew
  • 14,926
  • 3
  • 34
  • 54
1

Look at the following example:

Two threads increment a variable with the same function. If USE_ATOMIC is not defined, the increment itself would be done in an atomic copy of the var, so the increment itself is thread safe. But as you can see, the access to the volatile variable is not! If you run the example without USE_ATOMIC the result is undefined. If USE_ATOMIC is set, the result is always the same!

What happens is simple: volatile simply means that the variable can be changed out of the control of the compiler. This means, the compiler must read the variable before it modifies and write back the result. But this has simply nothing to do with synchronization. And more then this: On a multicore CPU the variable can exist two times ( in each of the cache for example ) and no cachs synchronization is done! There a lot more things which must be recognized in thread based programming. Here memory barrier is the missing topic.

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

//#define USE_ATOMIC

#ifdef USE_ATOMIC
std::atomic<long> i{0};
#else
volatile long i=0;
#endif

const long cnts=10000000;

void inc(volatile long &var)
{
    std::atomic<long> local_copy{var};
    local_copy++;
    var=local_copy;
}

void func1()
{
    long n=0;

    while ( n < cnts )
    {
        n++;
#ifdef USE_ATOMIC
        i++;
#else
        inc( i );
#endif
    }
}


int main()
{
    std::thread t1( func1 );
    std::thread t2( func1 );

    t1.join();
    t2.join();

    std::cout << i << std::endl;
}
Klaus
  • 24,205
  • 7
  • 58
  • 113