2

There is a thread discussing the definition of atomic object. The most popular interpretations are the followings:

  • By an "atomic object" we understand an object whose public interface exposes only atomic operations, i.e. all operations you can do with that object are atomic.
  • The C++ has the standard std::atomic<T> class template which fits the above descriptions.
  • The C++ standard imposes a set of rules on operations and effects of operations on atomic objects ([intro.races]). If all operations on an object satisfy those rules, then that object is atomic.

I see two possible interpretations for the atomic object:

  • An std::atomic<T> typed object
  • An underlying T typed object managed by an std::atomic<T> typed object

Based on the comments and in my understanding "atomic object" should refer an object with std::atomic<T> type - so the first interpretation is the correct one -, but it has serious consequences, like in the following context:

C++20 standard 6.9.2.1 (15):

If an operation A that modifies an atomic object M happens before an operation B that modifies M, then A shall be earlier than B in the modification order of M.

There is an std::atomic operation with the following footprint:

void notify_one() noexcept;

As it is not a const function it modifies the std::atomic object, but it doesn't modify the underlying T object. Does notify_one modify the atomic object M? (Does M refer the std::atomic<T> typed object, or its underlying T object?). If it does refer the std::atomic typed object then C++20 standard 6.9.2.1 (15) can be applied for the notify_one operations.

It is crucial to know to be able to answer questions like C++20: How is the returning from atomic::wait() guaranteed by the standard?.

The standard is confusing however, e.g. here, it weakens my reasoning and says atomic object is the managed object (at least in the case of atomic_ref):

An atomic_­ref object applies atomic operations ([atomics.general]) to the object referenced by *ptr such that, for the lifetime ([basic.life]) of the atomic_­ref object, the object referenced by *ptr is an atomic object ([intro.races]).

UPDATE

How could this interpretation affect wait/notify (C++20: How is the returning from atomic::wait() guaranteed by the standard?)? Let's suppose notify modifies the atomic object. That modification would be simply an indication that a wait should be woken up - and nothing to do with the current value managed by the atomic (so basically a futex wake that modifies the object, stores a flag, or whatever). As both store and notify are operations

that modifies an atomic object M, then A shall be earlier than B in the modification order of M

and

this effectively makes the cache coherence guarantee provided by most hardware available to C++ atomic operations.

So whenever notify is called after a store, the effect of that store operation is guaranteed to be observable on each and every waiting thread when that thread observes notify (as notify is later than store in the total order). If there is a waiting operation "M is eligible to be unblocked by a call to an atomic notifying operation" then notify must unblock it, and the value set by store has to be observable by the unblocked wait as an implication of 6.9.2.1 (15):

If an operation A that modifies an atomic object M happens before an operation B that modifies M, then A shall be earlier than B in the modification order of M.

and 6.9.2.1 (4):

All modifications to a particular atomic object M occur in some particular total order, called the modification order of M.

Broothy
  • 659
  • 5
  • 20
  • 2
    The issue has already been raised [here](https://github.com/cplusplus/CWG/issues/96). Also note that as said there, it only really makes sense for the modification order to apply to the managed object or its scalar subobjects regardless. I think it may be true that there should be a statement about data race avoidance for the non-`const` wait/notify functions. I am surprised that they are non-`const` to begin with. According to the commit message at the end of https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102994 nobody really seems to have cared either way. – user17732522 Aug 01 '22 at 11:22
  • @user17732522 Great to see! "Also note that as said there, it only really makes sense for the modification order to apply to the managed object regardless" - I disagree here. If the `notify_` operations would be part of the modification order related restrictions then the `wait/notify` synchronization would be solved automatically (as forcing the `store` operation before the `notify` would eliminate the need to synchronize the `wait` with `notify`, as the `store` and `notify` observation order would be the same on the waiting thread). – Broothy Aug 01 '22 at 11:36
  • @LanguageLawyer good question. The standard gives us a function footprint and a description what a function should do. It doesn't say explicitly that `notify` must modify the object, but it explicitly gives the permission to modify the object by marking the function non-const. `const` functions must not modify the object, but we don't have any modifier that forces the implementation to modify the object. The standard doesn't specify the exact algorithm, so -without knowing the exact implementation- it is the best indication I can figure out. If you have a better idea please share it. – Broothy Aug 01 '22 at 15:56
  • 1
    For `std::atomic`, unlike `atomic_ref`, there doesn't seem to be any indication that there even has to exist an "underlying" object of type `T`. We could imagine implementing `std::atomic` with a `long int` under the hood, and load and store doing appropriate conversions, or some deeper magic that doesn't map to C++ at all, with no object of type `char` anywhere in the picture. – Nate Eldredge Aug 01 '22 at 18:35
  • @NateEldredge I see your point, but what could be a better indication of possible modifications than marking the function non-const? Does `std::random_device::operator()` e.g. modify the object, or it just adjusts a global variable? It is up to the implementation I think, but as it is not a `const` function it could modify the object. The only hint the standard can give to us is whether the function is marked `const` or not. What does 'modify' mean in your interpretation? – Broothy Aug 01 '22 at 19:03
  • 1
    I do agree that `.notify()` *could* modify the object. But that alone does not resolve the wait/notify question from the other thread. In order to use modification order to prove that the wait will return, we would need to use read-read coherence, which requires that the `notify()` actually *does* modify the object, that it stores a value which was equal to the previous value in the modification order, that the unblock in the `wait()` is a value computation, and that it takes its value from the `notify()`. And I don't find support in the standard for any of those statements. – Nate Eldredge Aug 01 '22 at 19:13
  • @NateEldredge please see the update. I think you misunderstood how I think this interpretation could affect the `wait/notify` issue, I tried to be as detailed in my explanation as possible. – Broothy Aug 02 '22 at 04:31
  • @NateEldredge _read-read coherence, which requires that the `notify()` actually does modify the object_ Does it require? _the unblock in the `wait()` is a value computation, and that it takes its value from the `notify()`_ The unblock can take the value from the last Y before the `notify()` (https://timsong-cpp.github.io/cppwp/n4861/atomics.wait#4.3) – Language Lawyer Aug 02 '22 at 06:14
  • Prolly it will have to say «as if it takes the value», since, AFAIK, `volatile` bullshit still exists – Language Lawyer Aug 02 '22 at 06:21

0 Answers0