Atomic pointers in c++ and passing objects between threads

Michael picture Michael · Nov 6, 2014 · Viewed 27k times · Source

My question involves std::atomic<T*> and the data that this pointer points to. If in thread 1 I have

Object A;
std:atomic<Object*> ptr;
int bar = 2;
A.foo = 4;  //foo is an int;
ptr.store(*A);

and if in thread 2 I observe that ptr points to A, can I be guaranteed that ptr->foo is 4 and bar is 2?

Does the default memory model for the atomic pointer (sequentially consistent) guarantee that assignments on non-atomic (in this case A.foo) that happen before an atomic store will be seen by other threads before it sees the assignment of the same atomic.store for both cases?

If it helps or matters, I am using x64 (and I only care about this platform), gcc (with a version that supports atomics).

Answer

Christophe picture Christophe · Nov 6, 2014

The answer is yes and perhaps no

The memory model principles:

C++11 atomics use by default the std::memory_order_seq_cst memory ordering, which means that operations are sequentially consistent.

The semantics of this is that ordering of all operations are as if all these operations were performed sequentially :

  • C++ standard section 29.3/3 explains how this works for atomics: "There shall be a single total order S on all memory_order_seq_cst operations, consistent with the “happens before” order and modification orders for all affected locations, such that each memory_order_seq_cst operation that loads a value observes either the last preceding modification according to this order S, or the result of an operation that is not memory_order_seq_cst."

  • The section 1.10/5 explains how this impacts also non-atomics: "The library defines a number of atomic operations (...) that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another."

The answer to your question is yes !

Risk with non-atomic data

You shall however be aware that in reality the consistency guarantee is more limited for the non-atomic values.

Suppose a first execution scenario:

(thread 1) A.foo = 10; 
(thread 1) A.foo = 4;     //stores an int
(thread 1) ptr.store(&A); //ptr is set AND synchronisation 
(thread 2) int i = *ptr;  //ptr value is safely accessed (still &A) AND synchronisation

Here, i is 4. Because ptr is atomic, thread (2) safely gets the value &A when it reads the pointer. The memory ordering ensures that all assignments made BEFORE ptr are seen by the other threads ("happens before" constraint).

But suppose a second execution scenario:

(thread 1) A.foo = 4;     //stores an int
(thread 1) ptr.store(&A); //ptr is set AND synchronisation 
(thread 1) A.foo = 8;     // stores int but NO SYNCHRONISATION !! 
(thread 2) int i = *ptr;  //ptr value is safely accessed (still &A) AND synchronisation

Here the result is undefined. It could be 4 because of the memory ordering guaranteed that what happens before the ptr assignement is seen by the other threads. But nothing prevents assignments made afterwards to be seen as well. So it could be 8.

If you would have had *ptr = 8; instead of A.foo=8; then you would have certainty again: i would be 8.

You can verify this experimentally with this for example:

void f1() {  // to be launched in a thread
    secret = 50; 
    ptr = &secret; 
    secret = 777; 
    this_thread::yield();
}
void f2() { // to be launched in a second thread
    this_thread::sleep_for(chrono::seconds(2));
    int i = *ptr; 
    cout << "Value is " << i << endl;
}

Conclusions

To conclude, the answer to your question is yes, but only if no other change to the non atomic data happens after the synchronisation. The main risk is that only ptr is atomic. But this does not apply to the values pointed to.

To be noted that especially pointers bring further synchronisation risk when you reassign the atomic pointer to a non atomic pointer.

Example:

// Thread (1): 
std:atomic<Object*> ptr;
A.foo = 4;  //foo is an int;
ptr.store(*A);

// Thread (2): 
Object *x; 
x=ptr;      // ptr is atomic but x not !  
terrible_function(ptr);   // ptr is atomic, but the pointer argument for the function is not !