1

Following up to this question - std::memory_order_relaxed and initialization. Suppose I have code like this

class Something {
public:
  int value;
};
auto&& pointer = std::atomic<Something*>{nullptr};

// thread 1
auto value = Something{1};
pointer.set(&value, std::memory_order_relaxed);

// thread 2
Something* something = nullptr;
while (!(something = pointer.load(std::memory_order_relaxed))) {}
cout << something->value << endl;

Is this guaranteed to print 1? Can an implementation be allowed to take the address of a non initialized value?

(Assuming that there are no lifetime issues with thread 2 reading the pointer set by thread 1)

xskxzr
  • 12,442
  • 12
  • 37
  • 77
Curious
  • 20,870
  • 8
  • 61
  • 146
  • 1
    Since `pointer` is atomic, you will get a valid value.. However, the problem lies in the memory it is pointing at; that is unsynchronized, which means undefined behavior – LWimsey Jul 22 '18 at 20:01
  • @LWimsey I am not sure about that. C++ guarantees that an object is guaranteed to be constructed after you take its address on the stack. What I am unsure about is whether this applies across cache boundaries – Curious Jul 22 '18 at 20:02
  • 1
    That is true.. when thread 1 takes the address, it is pointing at a fully constructed object, but only visible to thread 1. If you want to use in tread 2 like this, stricter ordering is required. – LWimsey Jul 22 '18 at 20:06
  • @LWimsey could you point to some documentation or something that perhaps explains this requirement? – Curious Jul 22 '18 at 20:07
  • Working Draft N4750, section 6.8.2.1 (about multi-threaded executiongs) probably contains what you are looking for (a bit low-level though) – LWimsey Jul 22 '18 at 20:15
  • 3
    memory_order_relaxed imposes no constraints. So, it may be possible for the compiler to swap the lines in thread 1. For thread 1, it won't notice that it is taking the address of an unitialized variable because you don't use the object between those lines. However, it may lead to thread 2 reading the value before it is being initialized. – J. Calleja Jul 22 '18 at 20:29
  • Do you mean `pointer.store(..)`? `std::atomic` doesn't have a `.set` member function. – Peter Cordes Nov 21 '22 at 09:09

1 Answers1

2

No, it isn't guaranteed to print 1. The write to the field value may be reordered WRT to the write to pointer. If it is reordered to after the write to pointer, 'thread 2' will observe uninitialized memory.

This can and does happen in practice on ARM.

Because x86 CPUs maintain "total store order" (i.e. all stores are observable by other threads in the order they were issued by the issuing thread,) the CPU cannot cause this to happen. But, it still can happen on x86 because, while the CPU will not reorder writes, the compiler is allowed to reorder writes. I don't know if in practice compilers do that.

nmr
  • 16,625
  • 10
  • 53
  • 67
  • 1
    An x86 compiler can reorder those operations. Unlikely, unless surrounding code tempts it into doing so. That can happen with assignment to existing objects, coalescing dead stores like `x = 1;` ... `x = 2;` into just one store of `2` before or after some other store. So probably a later assignment to the newly constructed object could do it. Yup: https://godbolt.org/z/vPr67nqv4 shows just one store being fine, `value.value = 2;` after the atomic relaxed store making broken asm, and an atomic `release` store fixing it. (Although inevitable race on val=2 while a reader is loading the `1`) – Peter Cordes Nov 21 '22 at 09:17
  • 1
    Anyway yes, `release` and `acquire` memory ordering would guarantee correct operation, including forcing compilers to not reorder in bad ways. (`consume` would also work in the reader since this is a pointer being dereferenced.) – Peter Cordes Nov 21 '22 at 09:18
  • @PeterCordes any chance I can tempt you into answering https://stackoverflow.com/questions/74510609/do-dependent-reads-require-a-load-acquirem, getting a lot of colorful speculation – nmr Nov 21 '22 at 16:02