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 r1
s 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.