Atomic Operations in C
This article is intended for intermediate and advanced C programmers. It explains what atomic operations are, why they are important in multithreaded and embedded systems, and how the C11 atomic library provides a safe and efficient way to access shared data.
1. What Are Atomic Operations?
An atomic operation is an operation that is performed as a single, indivisible unit. Once an atomic operation begins, no other thread or execution context can observe it in a partially completed state or interrupt it before it finishes.
Atomic operations are essential whenever multiple execution contexts access the same data concurrently. These execution contexts may include multiple threads in a multithreaded application or an interrupt service routine (ISR) and the main program in an embedded system.
Consider the following statement:
Although it appears to be a single operation, it typically consists of several individual steps:
- Read the current value of
counterfrom memory. - Increment the value.
- Write the updated value back to memory.
If another thread or interrupt modifies counter between these steps, the final result may be incorrect.
An atomic increment performs the entire operation as a single indivisible action, ensuring that no other execution context can interfere while the operation is in progress. This guarantees that shared data remains consistent even when accessed concurrently.
2. Why Atomic Operations Are Needed
In many programs, multiple execution contexts may access the same variable at the same time. If one execution context modifies the variable while another is reading or updating it, the program may produce incorrect or unpredictable results.
For example, consider a shared counter that is incremented by two threads:
Although each thread executes the same statement, the increment operation is not inherently atomic. If both threads read the current value before either writes the updated value back to memory, one of the increments can be lost.
Suppose the value of counter is initially 10:
- Thread A reads
10. - Thread B reads
10. - Thread A increments the value to
11and writes it back. - Thread B increments its previously read value to
11and writes it back.
The final value of counter is 11 instead of the expected 12.
This type of error is known as a race condition, where the correctness of a program depends on the timing or ordering of concurrent operations.
Atomic operations eliminate this problem by ensuring that shared variables are updated as indivisible operations. Each update completes before another execution context can modify the same data, preventing lost updates and maintaining data consistency.
3. Atomic Types in C11
The C11 standard introduced the <stdatomic.h> header, which provides support for atomic operations and atomic data types. An atomic object guarantees that individual read, write, and read-modify-write operations are performed atomically, preventing data races when accessed concurrently.
There are two common ways to declare an atomic object:
- Using the
_Atomictype specifier. - Using one of the predefined atomic type aliases provided by
<stdatomic.h>.
For example, the following declarations create atomic integer variables:
Both declarations create an atomic integer and provide the same functionality. The atomic_int type is simply a convenience alias for _Atomic int.
The C11 standard provides predefined atomic type aliases for the most commonly used integer and character types.
| Atomic Type | Equivalent Type |
|---|---|
atomic_bool |
_Atomic bool |
atomic_char |
_Atomic char |
atomic_schar |
_Atomic signed char |
atomic_uchar |
_Atomic unsigned char |
atomic_short |
_Atomic short |
atomic_ushort |
_Atomic unsigned short |
atomic_int |
_Atomic int |
atomic_uint |
_Atomic unsigned int |
atomic_long |
_Atomic long |
atomic_ulong |
_Atomic unsigned long |
atomic_llong |
_Atomic long long |
atomic_ullong |
_Atomic unsigned long long |
atomic_char8_t* |
_Atomic char8_t |
atomic_char16_t |
_Atomic char16_t |
atomic_char32_t |
_Atomic char32_t |
atomic_wchar_t |
_Atomic wchar_t |
atomic_int_least8_t |
_Atomic int_least8_t |
atomic_uint_least8_t |
_Atomic uint_least8_t |
atomic_int_least16_t |
_Atomic int_least16_t |
atomic_uint_least16_t |
_Atomic uint_least16_t |
atomic_int_least32_t |
_Atomic int_least32_t |
atomic_uint_least32_t |
_Atomic uint_least32_t |
atomic_int_least64_t |
_Atomic int_least64_t |
atomic_uint_least64_t |
_Atomic uint_least64_t |
atomic_int_fast8_t |
_Atomic int_fast8_t |
atomic_uint_fast8_t |
_Atomic uint_fast8_t |
atomic_int_fast16_t |
_Atomic int_fast16_t |
atomic_uint_fast16_t |
_Atomic uint_fast16_t |
atomic_int_fast32_t |
_Atomic int_fast32_t |
atomic_uint_fast32_t |
_Atomic uint_fast32_t |
atomic_int_fast64_t |
_Atomic int_fast64_t |
atomic_uint_fast64_t |
_Atomic uint_fast64_t |
atomic_intptr_t |
_Atomic intptr_t |
atomic_uintptr_t |
_Atomic uintptr_t |
atomic_size_t |
_Atomic size_t |
atomic_ptrdiff_t |
_Atomic ptrdiff_t |
atomic_intmax_t |
_Atomic intmax_t |
atomic_uintmax_t |
_Atomic uintmax_t |
Note:
atomic_char8_tis available only whenchar8_tis supported by the implementation.
Atomic objects can also be created from user-defined types using the _Atomic type specifier.
For example:
Whether operations on user-defined atomic types are lock-free depends on the compiler and target architecture.
Once an object has been declared as atomic, it should be accessed using the atomic functions provided by <stdatomic.h>, such as atomic_load(), atomic_store(), and atomic_fetch_add(), which are discussed in the following sections.
4. Performing Atomic Operations
The <stdatomic.h> header provides a collection of functions for safely reading, writing, and modifying atomic objects. These operations guarantee that the access is performed atomically, preventing interference from other execution contexts.
Some of the most commonly used atomic operations are shown below.
Reading an Atomic Value
The atomic_load() function atomically reads the value of an atomic object.
Writing an Atomic Value
The atomic_store() function atomically writes a new value to an atomic object.
Exchanging Values
The atomic_exchange() function replaces the current value with a new value and returns the previous value.
After execution, old contains 10 and counter contains 20.
Compare-and-Exchange
The atomic_compare_exchange_*() functions compare the current value of an atomic object with an expected value. If they match, the object is updated atomically; otherwise, no update is performed.
This operation is widely used to implement lock-free data structures and synchronization algorithms.
5. Common Atomic Operations
In addition to reading and writing atomic objects, the C11 atomic library provides functions that perform common arithmetic and bitwise operations atomically. These functions are especially useful when multiple threads or execution contexts modify the same shared variable.
The following table summarizes the most commonly used atomic operations provided by <stdatomic.h>.
| Function | Description |
|---|---|
atomic_load() |
Atomically reads the value of an atomic object. |
atomic_store() |
Atomically writes a new value to an atomic object. |
atomic_exchange() |
Atomically replaces the current value with a new value and returns the previous value. |
atomic_compare_exchange_*() |
Atomically compares the current value with an expected value and updates it if they match. |
atomic_fetch_add() |
Atomically adds a value to an atomic object and returns the previous value. |
atomic_fetch_sub() |
Atomically subtracts a value from an atomic object and returns the previous value. |
atomic_fetch_and() |
Performs an atomic bitwise AND operation and returns the previous value. |
atomic_fetch_or() |
Performs an atomic bitwise OR operation and returns the previous value. |
atomic_fetch_xor() |
Performs an atomic bitwise XOR operation and returns the previous value. |
Atomic Load
The atomic_load() function atomically reads the value of an atomic object.
After execution, value contains 10.
Atomic Store
The atomic_store() function atomically writes a new value to an atomic object.
After execution, counter contains 20.
Atomic Exchange
The atomic_exchange() function atomically replaces the current value of an atomic object with a new value and returns the previous value.
After execution:
previouscontains10.countercontains20.
Compare-and-Exchange
The atomic_compare_exchange_strong() function compares the current value of an atomic object with an expected value. If the values match, the object is updated atomically.
#include <stdatomic.h>
#include <stdbool.h>
atomic_int counter = 10;
int expected = 10;
bool success = atomic_compare_exchange_strong(
&counter,
&expected,
20
);
If counter is equal to expected, it is updated to 20 and success is true. Otherwise, counter remains unchanged, success is false, and expected is updated with the current value of counter.
Atomic Addition
The atomic_fetch_add() function atomically adds a value to an atomic object and returns its previous value.
After execution:
previouscontains0.countercontains1.
Atomic Subtraction
The atomic_fetch_sub() function atomically subtracts a value from an atomic object.
Like atomic_fetch_add(), this operation is performed atomically, ensuring that concurrent updates to the shared variable are not lost.
Atomic Bitwise Operations
The C11 atomic library also provides atomic bitwise operations, including:
atomic_fetch_and()atomic_fetch_or()atomic_fetch_xor()
These operations are commonly used when manipulating shared status flags or hardware control bits.
For example, the following statement atomically sets bit 2 of flags:
Similarly, the following statement atomically clears bit 2 of flags:
The following statement atomically toggles bit 2 of flags:
These functions eliminate the need to perform separate read-modify-write sequences, which could otherwise introduce race conditions when shared data is accessed concurrently.
6. Atomic Operations in Embedded Systems
Atomic operations are not only useful in multithreaded applications but also play an important role in embedded systems. A common scenario involves shared variables that are accessed by both the main program and an Interrupt Service Routine (ISR).
For example, an ISR may increment a counter each time a hardware event occurs, while the main program periodically reads and processes the accumulated events.
#include <stdatomic.h>
atomic_int event_count = 0;
void Timer_IRQHandler(void)
{
atomic_fetch_add(&event_count, 1);
}
int main(void)
{
while (1)
{
if (atomic_load(&event_count) > 0)
{
atomic_fetch_sub(&event_count, 1);
/* Process one event */
}
}
}
Without atomic operations, both the ISR and the main program could attempt to update event_count simultaneously, potentially resulting in lost updates or inconsistent data.
Although the C11 atomic library provides a standardized interface for atomic operations, support depends on the compiler and target architecture. Some embedded compilers provide full support for <stdatomic.h>, while others offer only partial support or rely on hardware-specific instructions.
For targets that do not support C11 atomics, critical sections protected by temporarily disabling interrupts are commonly used to ensure atomic access to shared data.
7. Alternatives to C11 Atomic Operations
Prior to the introduction of the C11 standard, the C language did not provide a standardized mechanism for performing atomic operations. As a result, programmers relied on platform-specific synchronization techniques to safely access shared data.
Some of the most common alternatives include:
| Technique | Description |
|---|---|
| Mutexes | Commonly used in multithreaded applications to ensure that only one thread accesses shared data at a time. |
| Critical Sections | Frequently used in embedded systems by temporarily disabling interrupts while accessing shared data. |
| Compiler Intrinsics | Compiler-specific atomic functions, such as GCC's __atomic_*() and __sync_*() built-in functions, or Microsoft's _Interlocked*() functions. |
| Hardware Instructions | Some processors provide dedicated atomic instructions that are accessed through compiler intrinsics or assembly language. |
These techniques are still widely used today, particularly on systems where the C11 atomic library is not fully supported. However, they often reduce portability because they depend on the operating system, compiler, or processor architecture.
The <stdatomic.h> library introduced by C11 provides a standardized and portable interface for performing atomic operations. On supported platforms, the compiler translates these operations into the most appropriate synchronization mechanism, allowing the same source code to be used across different architectures with minimal modification.
8. Summary
Atomic operations are essential for safely accessing shared data in concurrent applications and embedded systems. Unlike ordinary read-modify-write operations, atomic operations are performed as indivisible units, preventing interference from other execution contexts.
The C11 standard introduced the <stdatomic.h> library, providing a portable set of atomic types and operations for reading, writing, and modifying shared objects. These facilities help eliminate race conditions and improve the reliability of software that relies on concurrent access to shared data.
In embedded systems, atomic operations are particularly useful when data is shared between the main program and Interrupt Service Routines (ISRs). Where supported, the C11 atomic library provides a standardized solution for managing shared data. On platforms without C11 atomic support, equivalent protection is often achieved using critical sections or hardware-specific synchronization mechanisms.
By understanding and correctly using atomic operations, developers can write safer, more robust, and more maintainable C programs.
Relevant Links
C/C++ Material on this website
(such as memory managment, pointers, embedded C programming etc.)