14

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).

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Michael
  • 1,321
  • 1
  • 13
  • 27
  • 6
    I think you mean `ptr.store(&A)`? `ptr.store(*A)` makes no sense (unless `Object` defines `Object * Object::operator*();`...). – cdhowie Nov 06 '14 at 18:50
  • 1
    Related: [C++11 introduced a standardized memory model. What does it mean? And how is it going to affect C++ programming?](http://stackoverflow.com/questions/6319146/c11-introduced-a-standardized-memory-model-what-does-it-mean-and-how-is-it-g) – Casey Nov 06 '14 at 19:58

2 Answers2

20

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 ! 
Christophe
  • 68,716
  • 7
  • 72
  • 138
  • I think, there is an ':' missing in line 1 – SoulfreezerXP Apr 03 '23 at 12:46
  • `Object *x = ptr;` is safe, it's doing an atomic load the same as `ptr.load()` to get a plain `Object*` (the `T` in `std::atomic`). Deref of the resulting `x` pointer is exactly the same as your `*ptr`, which is equivalent to `*(ptr.load())`. (std::atomic defines conversion operator to its value type as equivalent to `.load()`.) I think you're calling it unsafe because you're thinking it works like `reinterpret_cast( &atomic_ptr )` or something, which would do a non-atomic access to the shared memory if you dereferenced it. But that's not the case. – Peter Cordes Apr 03 '23 at 22:07
  • In your "second scenario" where the writer thread does another store to the payload after updating the pointer, it's not just the result that's undefined. The *behaviour* is undefined, of the whole program, because that's data-race UB. In practice on real CPUs, yes it's extremely likely that you'll just get either `4` or `8`, but unsynchronized write+read is UB in C++. – Peter Cordes Apr 03 '23 at 22:10
  • BTW, in your last code block, `ptr.store(*A);` is supposed to be `ptr.store(&A);` - the code in the question got that wrong but you didn't comment on it. – Peter Cordes Apr 03 '23 at 22:11
  • Also, the other way for this to break is if the reader did `ptr.load(std::memory_order_relaxed)`. Which in theory doesn't guarantee ordering since that's even weaker than `memory_order_consume`, but in practice the data dependency will create ordering except on DEC Alpha. Or if the compiler was able to prove that the only possible value `Object *x` could have was `&A`, and optimized away the data dependency. e.g. if you did `if(x == &A){ return x->foo; }` it could optimize to `A.foo`. – Peter Cordes Apr 03 '23 at 22:15
  • @PeterCordes can you please provide the reference to the C++ standard that guarantees that `Object *x = ptr;` is atomic ? – Christophe Apr 04 '23 at 06:08
  • `std::atomic::operator T()` is "equivalent to `load()`". https://en.cppreference.com/w/cpp/atomic/atomic/operator_T . Dereferencing the plain pointer you get from the load is not atomic, just like it's not when you do it as part of the same expression like `*ptr` or `*ptr.load()`. The atomic object holds a pointer, accesses to that pointer value are atomic. Deref of the resulting pointer to get to the final `Object` are not atomic. – Peter Cordes Apr 04 '23 at 06:14
  • That's why it's data-race UB for the writer to do `A.foo = 8` after publishing a pointer to that non-atomic object, not just an uncertain result like if you had `std::atomic A`. – Peter Cordes Apr 04 '23 at 06:16
7

By default, C++-11 atomic operations have acquire/release semantics.

So a thread that see's your store will also see all operations performed before it.

You can find some more details here.

JAAD
  • 12,349
  • 7
  • 36
  • 57
David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • 12
    +1. (Actually the default is `std::memory_order_seq_cst`, which has even stronger guarantees than acquire/release, but indeed that's all that would be required here anyway.) – Cameron Nov 06 '14 at 19:53