Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ : std::atomic<bool> and volatile bool

Tags:

I'm just reading the C++ concurrency in action book by Anthony Williams. There is this classic example with two threads, one produce data, the other one consumes the data and A.W. wrote that code pretty clear :

std::vector<int> data;
std::atomic<bool> data_ready(false);

void reader_thread()
{
    while(!data_ready.load())
    {
        std::this_thread::sleep(std::milliseconds(1));
    }
    std::cout << "The answer=" << data[0] << "\n";
}

void writer_thread()
{
    data.push_back(42);
    data_ready = true;
}

And I really don't understand why this code differs from one where I'd use a classic volatile bool instead of the atomic one. If someone could open my mind on the subject, I'd be grateful. Thanks.

like image 432
jedib Avatar asked Apr 14 '15 16:04

jedib


People also ask

What is atomic bool?

AtomicBoolean class provides operations on underlying boolean value that can be read and written atomically, and also contains advanced atomic operations. AtomicBoolean supports atomic operations on underlying boolean variable. It have get and set methods that work like reads and writes on volatile variables.

Does STD Atomic need volatile?

Atomic variable examples In order to use atomicity in your program, use the template argument std::atomic on the attributes. Note, that you can't make your whole class atomic, just it's attributes. }; You don't need to use volatile along with std::atomic .

Can bool be volatile?

Only the following types can be marked volatile : all reference types, Single , Boolean , Byte , SByte , Int16 , UInt16 , Int32 , UInt32 , Char , and all enumerated types with an underlying type of Byte , SByte , Int16 , UInt16 , Int32 , or UInt32 .

What is std :: atomic in C++?

(since C++11) Each instantiation and full specialization of the std::atomic template defines an atomic type. Objects of atomic types are the only C++ objects that are free from data races; that is, if one thread writes to an atomic object while another thread reads from it, the behavior is well-defined.


2 Answers

The big difference is that this code is correct, while the version with bool instead of atomic<bool> has undefined behavior.

These two lines of code create a race condition (formally, a conflict) because they read from and write to the same variable:

Reader

while (!data_ready)

And writer

data_ready = true;

And a race condition on a normal variable causes undefined behavior, according to the C++11 memory model.

The rules are found in section 1.10 of the Standard, the most relevant being:

Two actions are potentially concurrent if

  • they are performed by different threads, or
  • they are unsequenced, and at least one is performed by a signal handler.

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, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

You can see that whether the variable is atomic<bool> makes a very big difference to this rule.

like image 114
Ben Voigt Avatar answered Sep 17 '22 17:09

Ben Voigt


A "classic" bool, as you put it, would not work reliably (if at all). One reason for this is that the compiler could (and most likely does, at least with optimizations enabled) load data_ready only once from memory, because there is no indication that it ever changes in the context of reader_thread.

You could work around this problem by using volatile bool to enforce loading it every time (which would probably seem to work) but this would still be undefined behavior regarding the C++ standard because the access to the variable is neither synchronized nor atomic.

You could enforce synchronization using the locking facilities from the mutex header, but this would introduce (in your example) unnecessary overhead (hence std::atomic).


The problem with volatile is that it only guarantees that instructions are not omitted and the instruction ordering is preserved. volatile does not guarantee a memory barrier to enforce cache coherence. What this means is that writer_thread on processor A can write the value to it's cache (and maybe even to the main memory) without reader_thread on processor B seeing it, because the cache of processor B is not consistent with the cache of processor A. For a more thorough explanation see memory barrier and cache coherence on Wikipedia.


There can be additional problems with more complex expressions than x = y (i.e. x += y) that would require synchronization through a lock (or in this simple case an atomic +=) to ensure the value of x does not change during processing.

x += y for example is actually:

  • read x
  • compute x + y
  • write result back to x

If a context switch to another thread occurs during the computation this can result in something like this (2 threads, both doing x += 2; assuming x = 0):

Thread A                 Thread B
------------------------ ------------------------
read x (0)
compute x (0) + 2
                 <context switch>
                         read x (0)
                         compute x (0) + 2
                         write x (2)
                 <context switch>
write x (2)

Now x = 2 even though there were two += 2 computations. This effect is known as tearing.

like image 41
Max Truxa Avatar answered Sep 19 '22 17:09

Max Truxa