Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't std::atomic initialisation do atomic release so other threads can see the initialised value?

Something very odd turned up during the thread sanitising of proposed boost::concurrent_unordered_map and is recounted at this blog post. In short, bucket_type looks like this:

  struct bucket_type_impl
  {
    spinlock<unsigned char> lock;  // = 2 if you need to reload the bucket list
    atomic<unsigned> count; // count is used items in there
    std::vector<item_type, item_type_allocator> items;
    bucket_type_impl() : count(0), items(0) {  }
    ...

Yet the thread sanitiser claims that there is a race between the construction of a bucket_type and its first use, specifically when the count atomic is loaded from. It turns out that if you initialise a std::atomic<> via its constructor, that initialisation is not atomic and therefore the memory location is not atomically released and therefore not visible to other threads, which is counterintuitive given it's an atomic, and that most atomic operations default to memory_order_seq_cst. You must therefore explicitly do a release store after construction to initialise the atomic with a value visible to other threads.

Is there some extremely pressing reason why std::atomic with a value consuming constructor does not initialise itself with release semantics? If not, I think this is a library defect.

Edit: Jonathan's answer is the better for the history as to why, but ecatmur's answer links to Alastair's defect report on the matter, and how it was closed by simply adding a note to say construction offers no visibility to other threads. I'll therefore award the answer to ecatmur. Thanks to all who replied, I think the way is clear to ask for an extra constructor, it will at least stand out in the documentation that there is something unusual with the value consuming constructor.

Edit 2: I ended up raising this as a defect in the C++ language with the committee, and Hans Boehm who chairs the Concurrency part feels this is not an issue for the following reasons:

  1. No present C++ compiler in 2014 treats consume as different to acquire. As you will never, in real world code, pass an atomic to another thread without going through some release/acquire, the initialisation of the atomic would be made visible to all threads using the atomic. I think this fine until compilers catch up, and before that the Thread Sanitiser will warn on this.

  2. If you're doing mismatched consume-acquire-release like I am (I am using a release-inside-lock/consume-outside-lock atomic to speculatively avoid a release-acquire spinlock where it was unnecessary) then you're a big enough boy to know you must manually store release atomics after construction. That is probably a fair point.

like image 718
Niall Douglas Avatar asked Sep 01 '14 16:09

Niall Douglas


People also ask

Can std :: atomic be moved?

std::atomic is neither copyable nor movable.

Are atomic variables thread safe C++?

In order to solve this problem, C++ offers atomic variables that are thread-safe. The atomic type is implemented using mutex locks. If one thread acquires the mutex lock, then no other thread can acquire it until it is released by that particular thread.

What does std :: atomic do?

Module std::sync::atomic. Atomic types provide primitive shared-memory communication between threads, and are the building blocks of other concurrent types. Rust atomics currently follow the same rules as C++20 atomics, specifically atomic_ref .


2 Answers

It's because the converting constructor is constexpr, and constexpr functions can't have side effects such as atomic semantics.

In DR846, Alastair Meredith writes:

I'm not sure if the initialization is implied by use of constexpr keyword (which restricts the form of a constructor) but even if that is the case, I think it is worth spelling out explicitly as the inference would be far too subtle in that case.

The resolution for that defect (by Lawrence Crowl) was to document the constructor with the note:

[Note: Construction is not atomic. —end note]

The note was then expanded to the current wording, giving an example of a possible memory race (via memory_order_relaxed operations communicating the address of the atomic) in DR1478.

The reason that the converting constructor needs to be constexpr is (primarily) to allow static initialization. In DR768 we see:

Further discussion: why is the ctor labeled "constexpr"? Lawrence [Crowl] said this permits the object to be statically initialized, and that's important because otherwise there would be a race condition on initialization.

So: making the constructor constexpr eliminates race conditions on static-lifetime objects, at the cost of a race in dynamic-lifetime objects that only occurs in fairly contrived situations, since for a race to occur the memory location of the dynamic-lifetime atomic object must be communicated to another thread in a way that does not result in the value of the atomic object being also synchronized to that thread.

like image 115
ecatmur Avatar answered Sep 29 '22 16:09

ecatmur


That is an intentional design choice (there's even a note in the standard warning about it) and I think it was done in an attempt to be compatible with C.

The C++11 atomics were designed so that they could be used by WG14 for C as well, using the non-member functions such as atomic_load with types such as atomic_int rather than the member functions of the C++-only std::atomic<int>. In the original design, the atomic_int type has no special properties and atomicity is only achieved through atomic_load() and other functions. In that model atomic_init is not an atomic operation, it just initializes a POD. Only a subsequent atomic_store(&i, 1) call would be atomic.

In the end, WG14 decided to do things differently, adding the _Atomic specifier which makes the atomic_int type have magic properties. I'm not sure whether that means initialization of C atomics could be atomic (as it stands, atomic_init in C11 and C++11 is documented to be non-atomic), so maybe the C++11 rule is unnecessary. I suspect people will argue that there are good performance reason to keep initialization non-atomic, as interjay's comment above says, you need to send some notification to the other thread that the obejct is constructed and ready to be read from, so that notification could introduce the necessary fencing. Doing it once for the std::atomic initialization and then a second time to say the object is constructed could be wasteful.

like image 24
Jonathan Wakely Avatar answered Sep 29 '22 16:09

Jonathan Wakely