Is Assignment Operator '=' Atomic

is assignment operator '=' atomic?

This code is not guaranteed to be thread-safe on Win32, since Win32 guarantees atomicity only for properly-aligned 4-byte and pointer-sized values. bool is not guaranteed to be one of those types. (It is typically a 1-byte type.)

For those who demand an actual example of how this could fail:

Suppose that bool is a 1-byte type. Suppose also that your is_true variable happens to be stored adjacent to another bool variable (let's call it other_bool), so that both of them share the same 4-byte line. For concreteness, let's say that is_true is at address 0x1000 and other_bool is at address 0x1001. Suppose that both values are initially false, and one thread decides to update is_true at the same time another thread tries to update other_bool. The following sequence of operations can occur:

  • Thread 1 prepares to set is_true to true by loading the 4-byte value containing is_true and other_bool. Thread 1 reads 0x00000000.
  • Thread 2 prepares to set other_bool to true by loading the 4-byte value containing is_true and other_bool. Thread 2 reads 0x00000000.
  • Thread 1 updates the byte in the 4-byte value corresponding to is_true, producing 0x00000001.
  • Thread 2 updates the byte in the 4-byte value corresponding to other_bool, producing 0x00000100.
  • Thread 1 stores the updated value to memory. is_true is now true and other_bool is now false.
  • Thread 2 stores the updated value to memory. is_true is now false and other_bool is now true.

Observe that at the end this sequence, the update to is_true was lost, because it was overwritten by thread 2, which captured an old value of is_true.

It so happens that x86 is very forgiving of this type of error because it supports byte-granular updates and has a very tight memory model. Other Win32 processors are not as forgiving. RISC chips, for example, often do not support byte-granular updates, and even if they do, they usually have very weak memory models.

Atomic Assignment Operator

if mValue is a primitive type (and at most 32 bits wide on a 32-bit CPU, at most 64 bits wide on a 64-bit CPU), and you're running on an x86 CPU (in 32 or 64 bit mode) and you don't manually misalign data, then memory reads/writes are guaranteed to be atomic.

This does not, in itself, mean that the compiler won't reorder memory accesses or even optimize it out entirely, but the CPU does guarantee that any well-aligned read/write with data of those sizes will be atomic.

However, note that I'm talking about atomicity, not thread safety, because "thread safety" depends on the context in which the code is used.

Atomicity of the simple assignment operator

Following the example in this Dr Dobbs article, simple assignment of atomic variables in C11 is atomic.

The C11 standard (ISO/IEC 9899:2011), section 6.2.6.1/9 reads:

Loads and stores of objects with atomic types are done with
memory_order_seq_cst semantics.

In addition to being atomic, operations performed with memory_order_seq_cst semantics have a single ordering observed by all threads (aka sequentially-consistent ordering).

Without the _Atomic type qualifier, it is possible for an assignment to be non-atomic. Assigning a 64 bit value (e.g. a long long) on a 32 bit machine requires two CPU cycles. If another thread reads the value between those two cycles they'll get 4 bytes of the old value and 4 bytes of the new value.

Openmp atomic and critical

According to the docs here you can only use atomic with certain statement forms:

Sample Image

Also, make sure the comparison is inside the critsec! So I assume you cannot have what you want, but if you had

if(prod > final_prod) // unsynchronized read
{
#pragma omp critical
final_prod = prod;
}

it would still be data race

Use of deleted function error with std::atomic_int

Your code is attempting to construct a temporary std::atomic_int on the RHS, then use the std::atomic_int copy constructor (which is deleted) to initialise stop, like so:

std::atomic_int stop = std::atomic_int(0);

That's because copy-initialisation, as you are performing here, is not quite equivalent to other kinds of initialisation.

[C++11: 8.5/16]: The semantics of initializers are as follows [..]

If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).

(this allows for option 3, at the end of this answer)

[..]

If the destination type is a (possibly cv-qualified) class type:

  • If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (13.3.1.3), and the best one is chosen through overload resolution (13.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

(this almost describes your code but not quite; the key here is that, perhaps contrary to intuition, std::atomic_int's constructors are not considered at all in your case!)

  • Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized; see 12.2, 12.8.

(this is your scenario; so, although the copy can be elided, it still must be possible)

  • [..]

Here's the fix, anyway; use direct-initialisation or list-initialisation:

std::atomic_int stop(0);     // option 1
std::atomic_int stop{0}; // option 2
std::atomic_int stop = {0}; // option 3

Why is reference assignment atomic in Java?

First of all, reference assignment is atomic because the specification says so. Besides that, there is no obstacle for JVM implementors to fulfill this constraint, as 64 Bit references are usually only used on 64 Bit architectures, where atomic 64 Bit assignment comes for free.

Your main confusion stems from the assumption that the additional “Atomic References” feature means exactly that, due to its name. But the AtomicReference class offers far more, as it encapsulates a volatile reference, which has stronger memory visibility guarantees in a multi-threaded execution.

Having an atomic reference update does not necessarily imply that a thread reading the reference will also see consistent values regarding the fields of the object reachable through that reference. All it guarantees is that you will read either the null reference or a valid reference to an existing object that actually was stored by some thread. If you want more guarantees, you need constructs like synchronization, volatile references, or an AtomicReference.

AtomicReference also offers atomic update operations like compareAndSet or getAndSet. These are not possible with ordinary reference variables using built-in language constructs (but only with special classes like AtomicReferenceFieldUpdater or VarHandle).



Related Topics



Leave a reply



Submit