Home » C++ » Are C/C++ fundamental types atomic?

Are C/C++ fundamental types atomic?

Posted by: admin November 30, 2017 Leave a comment

Questions:

Are C/C++ fundamental types, like int, double, etc., atomic, e.g. threadsafe?

Are they free from data races; that is, if one thread writes to an object of such a type while another thread reads from it, is the behavior well-defined?

If not, does it depend on the compiler or something else?

Answers:

No, fundamental data types (e.g., int, double) are not atomic, see std::atomic.

Instead you can use std::atomic<int> or std::atomic<double>.

Note: std::atomic was introduced with C++11 and my understanding is that prior to C++11, the C++ standard didn’t recognize the existence of multithreading at all.


As pointed out by @Josh, std::atomic_flag is an atomic boolean type. It is guaranteed to be lock-free, unlike the std::atomic specializations.


The quoted documentation is from: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf. I’m pretty sure the standard is not free and therefore this isn’t the final/official version.

1.10 Multi-threaded executions and data races

  1. Two expression evaluations conflict if one of them modifies a memory location (1.7) and the other one reads or modifies the same memory location.
  2. The library defines a number of atomic operations (Clause 29) and operations on mutexes (Clause 30) that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another. A synchronization operation on one or more memory locations is either a consume operation, an acquire operation, a release operation, or both an acquire and release operation. A synchronization operation without an associated memory location is a fence and can be either an acquire fence, a release fence, or both an acquire and release fence. In addition, there are relaxed atomic operations, which are not synchronization operations, and atomic read-modify-write operations, which have special characteristics.

  1. Two actions are potentially concurrent if

    (23.1) — they are performed by different threads, or

    (23.2) — they are unsequenced, and at least one is performed by a signal handler.

    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

29.5 Atomic types

  1. There shall be explicit specializations of the atomic template for the integral types “char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, char16_t, char32_t, wchar_t, and any other types needed by the typedefs in the header <cstdint>. For each integral type integral, the specialization atomic<integral> provides additional atomic operations appropriate to integral types. There shall be a specialization atomic<bool> which provides the general atomic operations as specified in 29.6.1..

  1. There shall be pointer partial specializations of the atomic class template. These specializations shall have standard layout, trivial default constructors, and trivial destructors. They shall each support aggregate initialization syntax.

29.7 Flag type and operations

  1. Operations on an object of type atomic_flag shall be lock-free. [ Note: Hence the operations should also be address-free. No other type requires lock-free operations, so the atomic_flag type is the minimum hardware-implemented type needed to conform to this International standard. The remaining types can be emulated with atomic_flag, though with less than ideal properties. — end note ]
Questions:
Answers:

Since C is also (currently) mentioned in the question despite not being in the tags, the C Standard states:

5.1.2.3 Program execution

When the processing of the abstract machine is interrupted by receipt
of a signal, the values of objects that are neither lock-free atomic
objects nor of type volatile sig_atomic_t are unspecified, as is the
state of the floating-point environment. The value of any object
modified by the handler that is neither a lock-free atomic object nor
of type volatile sig_atomic_t becomes indeterminate when the handler
exits, as does the state of the floating-point environment if it is
modified by the handler and not restored to its original state.

and

5.1.2.4 Multi-threaded executions and data races

Two expression evaluations
conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.

[several pages of standards – some paragraphs explicitly addressing atomic types]

The execution of a program contains a
data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens
before the other. Any such data race results in undefined behavior.

Note that values are “indeterminate” if a signal interrupts processing, and simultaneous access to types that are not explicitly atomic is undefined behavior.

Questions:
Answers:

What is atomic?

Atomic, as describing something with the property of an atom. The word atom originates from Latin atomus meaning “undivided”.

Typically I think of an atomic operation (regardless of language) to have two qualities:

An atomic operation is always undivided.

I.e. it is performed in an indivisible way, I believe this is what OP refers to as “threadsafe”. In a sense the operation happens instantaneously when viewed by another thread.

For example the following operation is likely divided (compiler/hardware dependent):

i += 1;

because it can be observed by another thread (on hypothetical hardware and compiler) as:

load r1, i;
addi r1, #1;
store i, r1;

Two threads doing the above operation i += 1 without appropriate synchronization may produce the wrong result. Say i=0 initially, thread T1 loads T1.r1 = 0, and the thread T2 loads t2.r1 = 0. Both threads increment their respective r1s by 1 and then store the result to i. Although two increments have been performed, the value of i is still only 1 because the increment operation was divisible. Note that had there been synchronization before and after i+=1 the other thread would have waited until the operation was complete and thus would have observed an undivided operation.

Note that even a simple write may or may not be undivided:

i = 3;

store i, #3;

depending on the compiler and hardware. For example if the address of i is not aligned suitably, then an unaligned load/store has to be used which is executed by the CPU as several smaller loads/stores.

An atomic operation has guaranteed memory ordering semantics.

Non atomic operations may be re-ordered and may not necessarily occur in the order written in the program source code.

For example, under the “as-if” rule the compiler is allowed to re-order stores and loads as it sees fit as long as all access to volatile memory occurs in the order specified by the program “as if” the program was evaluated according to the wording in the standard. Thus non-atomic operations may be re-arranged breaking any assumptions about execution order in a multi-threaded program. This is why a seemingly innocent use of a raw int as a signaling variable in multi-threaded programming is broken, even if writes and reads may be indivisible, the ordering may break the program depending on the compiler. An atomic operation enforces ordering of the operations around it depending on what memory semantics are specified. See std::memory_order.

The CPU may also re-order your memory accesses under the memory ordering constraints of that CPU. You can find the memory ordering constraints for the x86 architecture in the Intel 64 and IA32 Architectures Software Developer Manual section 8.2 starting at page 2212.

Primitive types (int, char etc) are not Atomic

Because even if they under certain conditions may have indivisible store and load instructions or possibly even some arithmetic instructions, they do not guarantee the ordering of stores and loads. As such they are unsafe to use in multi-threaded contexts without proper synchronization to guarantee that the memory state observed by other threads is what you think it is at that point in time.

I hope this explains why primitive types are not atomic.

Questions:
Answers:

An additional info I haven’t seen mentioned in the other answers so far:

If you use std::atomic<bool>, for example, and bool is actually atomic on the target architecture, then the compiler will not generate any redundant fences or locks. The same code would be generated as for a plain bool.

In other words, using std::atomic only makes the code less efficient if it is actually required to for correctness on the platform. So there is no reason to avoid it.