NB: For this question, I'm not talking about the C or C++ language standards. Rather, I'm talking about gcc compiler implementations for a particular architecture, as the only guarantees for atomicity by the language standards are to use _Atomic
types in C11 or later or std::atomic<>
types in C++11 or later. See also my updates at the bottom of this question.
On any architecture, some data types can be read atomically, and written atomically, while others will take multiple clock cycles and can be interrupted in the middle of the operation, causing corruption if that data is being shared across threads.
On 8-bit single-core AVR microcontrollers (ex: the ATmega328 mcu, used by the Arduino Uno, Nano, or Mini), only 8-bit data types have atomic reads and writes (with the gcc compiler and gnu C or gnu C++ language). I had a 25-hr debugging marathon in < 2 days and then wrote this answer here. See also the bottom of this question for more info. and documentation on 8-bit variables having naturally atomic writes and naturally atomic reads for AVR 8-bit microcontrollers when compiled with the gcc compiler which uses the AVR-libc library.
On (32-bit) STM32 single-core microcontrollers, any data type 32-bits or smaller is definitively automatically atomic (when compiled with the gcc compiler and the gnu C or gnu C++ language, as ISO C and C++ make no guarantees of this until the 2011 versions with _Atomic
types in C11 and std::atomic<>
types in C++11). That includes bool
/_Bool
, int8_t
/uint8_t
, int16_t
/uint16_t
, int32_t
/uint32_t
, float
, and all pointers. The only not atomic types are int64_t
/uint64_t
, double
(8 bytes), and long double
(also 8 bytes). I wrote about that here:
- Which variable types/sizes are atomic on STM32 microcontrollers?
- Reading a 64 bit variable that is updated by an ISR
- What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?
Now I need to know for my 64-bit Linux computer. Which types are definitively automatically atomic?
My computer has an x86-64 processor, and Linux Ubuntu OS.
I am okay using Linux headers and gcc extensions.
I see a couple of interesting things in the gcc source code indicating that at least the 32-bit int
type is atomic. Ex: the Gnu++ header <bits/atomic_word.h>
, which is stored at /usr/include/x86_64-linux-gnu/c++/8/bits/atomic_word.h
on my computer, and is here online, contains this:
typedef int _Atomic_word;
So, int
is clearly atomic.
And the Gnu++ header <bits/types.h>
, included by <ext/atomicity.h>
, and stored at /usr/include/x86_64-linux-gnu/bits/types.h
on my computer, contains this:
/* C99: An integer type that can be accessed as an atomic entity,
even in the presence of asynchronous interrupts.
It is not currently necessary for this to be machine-specific. */
typedef int __sig_atomic_t;
So, again, int
is clearly atomic.
Here is some sample code to show what I am talking about...
...when I say that I want to know which types have naturally atomic reads, and naturally atomic writes, but not atomic increment, decrement, or compound assignment.
volatile bool shared_bool;
volatile uint8_t shared u8;
volatile uint16_t shared_u16;
volatile uint32_t shared_u32;
volatile uint64_t shared_u64;
volatile float shared_f; // 32-bits
volatile double shared_d; // 64-bits
// Task (thread) 1
while (true)
{
// Write to the values in this thread.
//
// What I write to each variable will vary. Since other threads are reading
// these values, I need to ensure my *writes* are atomic, or else I must
// use a mutex to prevent another thread from reading a variable in the
// middle of this thread's writing.
shared_bool = true;
shared_u8 = 129;
shared_u16 = 10108;
shared_u32 = 130890;
shared_f = 1083.108;
shared_d = 382.10830;
}
// Task (thread) 2
while (true)
{
// Read from the values in this thread.
//
// What thread 1 writes into these values can change at any time, so I need
// to ensure my *reads* are atomic, or else I'll need to use a mutex to
// prevent the other thread from writing to a variable in the midst of
// reading it in this thread.
if (shared_bool == whatever)
{
// do something
}
if (shared_u8 == whatever)
{
// do something
}
if (shared_u16 == whatever)
{
// do something
}
if (shared_u32 == whatever)
{
// do something
}
if (shared_u64 == whatever)
{
// do something
}
if (shared_f == whatever)
{
// do something
}
if (shared_d == whatever)
{
// do something
}
}
C _Atomic
types and C++ std::atomic<>
types
I know C11 and later offers _Atomic
types, such as this:
const _Atomic int32_t i;
// or (same thing)
const atomic_int_least32_t i;
See here:
And C++11 and later offers std::atomic<>
types, such as this:
const std::atomic<int32_t> i;
// or (same thing)
const atomic_int32_t i;
See here:
And these C11 and C++11 "atomic" types offer atomic reads and atomic writes as well as atomic increment operator, decrement operator, and compound assignment...
...but that's not really what I'm talking about.
I want to know which types have naturally atomic reads and naturally atomic writes only. For what I am talking about, increment, decrement, and compound assignment will not be naturally atomic.
Update 14 Apr. 2022
I had some chats with someone from ST, and it seems the STM32 microcontrollers only guarantee atomic reads and writes for variables of certain sizes under these conditions:
- You use assembly.
- You use the C11
_Atomic
types or the C++11std::atomic<>
types. - You use the gcc compiler with gnu language and gcc extensions.
- I'm most interested in this last one, since that's what the crux of my assumptions at the top of this question seem to have been based on for the last 10 years, without me realizing it. I'd like help finding the gcc compiler manual and the places in it where it explains these atomic access guarantees that apparently exist. We should check the:
- AVR gcc compiler manual for 8-bit AVR ATmega microcontrollers.
- STM32 gcc compiler manual for 32-bit ST microcontrollers.
- x86-64 gcc compiler manual??--if such a thing exists, for my 64-bit Ubuntu computer.
- I'm most interested in this last one, since that's what the crux of my assumptions at the top of this question seem to have been based on for the last 10 years, without me realizing it. I'd like help finding the gcc compiler manual and the places in it where it explains these atomic access guarantees that apparently exist. We should check the:
My research thus far:
AVR gcc: no avr gcc compiler manual exists. Rather, use the AVR-libc manual here: https://www.nongnu.org/avr-libc/ --> "Users Manual" links.
- The AVR-libc user manual in the
<util/atomic>
section backs up my claim that 8-bit types on AVR, when compiled by gcc, already have naturally atomic reads and naturally atomic writes when it implies that 8-bit reads and writes are already atomic by saying (emphasis added):
A typical example that requires atomic access is a 16 (or more) bit variable that is shared between the main execution path and an ISR.
- It is talking about C code, not assembly, as all examples it gives on that page are in C, including the one for the
volatile uint16_t ctr
variable, immediately following that quote.
- The AVR-libc user manual in the
is_always_lock_free
to detect which types are atomically readable/updatable. (And you have to useatomic<>
to get the atomic behavior.) – Sairestd::atomic<T>::is_always_lock_free()
could be useful. What does it mean to be "lock free", exactly? An answer could be useful. Also, is there an equivalent to this in C? I frequently use both languages and would like to know a C solution too. – Thildestd::atomic<>
goes beyond just simple atomic reads and writes. See here: en.cppreference.com/w/cpp/atomic/atomic. It offers atomic increment, decrement, and compound assignment such asoperator+=
, too, meaning, I suspect they are using locking of some sort under-the-hood in the implementation of that. – Thildeint
). – Predesignatestd::atomic
. So the question might be, which atomic types are lock free? But that's not all, there's atomic operations on atomic types which aren't a single instruction even if it's lock free. – OverflightThe language says
. I'm not asking what the language says though, exactly. The language for AVR is C or C++, and the language for STM32 is C or C++, yet the hardware says what types are atomic there, and we have definitive answers for the languages, despite the languages not specifying. In other words, for the languages, the answer is likely unspecified. But, for the compiler on a given architecture, it is likely well-defined, like in AVR and STM32. I see your points about lock free types and multiple instruction-types though. – Thildestd::atomic<>::is_always_lock_free()
do that? I don't really know what "always lock free" means, and unfortunately that call acts onstd::atomic<>
types only, not regular types. – Thildestd::atomic<>::is_always_lock_free()
returns true iff the compiler can guarantee that that std::atomic type will never require the implicit locking/unlocking of a mutex to implement its atomicity guarantees. It's probably what you want. – Arlenarlenabool
semaphore which must be guaranteed to be atomic. You must either acknowledge thatbool
semaphore is useless, or acknowledge that the 8-bit write is atomic. As it is written, it is contradictory. I say the 8-bit write is atomic. – Thildestatic_assert(std::atomic<int32_t>::is_always_lock_free())
to verify that the compiler supports the underlying CPU operation, and then usevalue.load(std::memory_order_relaxed)
to perform an unordered read orvalue.store(newvalue, std::memory_order_relaxed)
to perform an unordered write. Unordered reads/writes almost always compile to a single load or store instruction. – Saire