Back to C Programming
2026-02-087 min read

Atomic types

Learn Atomic types step by step with clear examples and exercises.

Why This Matters

Atomic types are crucial in multi-threaded programming to ensure data consistency and prevent unexpected behavior or bugs. By providing atomic operations on variables, they help eliminate data races and make it possible to manipulate shared data concurrently without causing inconsistencies. Understanding atomic types is essential for writing efficient, reliable, and scalable concurrent programs.

Prerequisites

Before diving into atomic types, you should be familiar with the following concepts:

  1. Basic C programming: variables, functions, pointers, arrays, structures, etc.
  2. Concurrency in C: threads, mutexes, condition variables, and their usage for synchronization.
  3. Understanding of memory models and data races.
  4. Familiarity with the C11 standard and its new features.

Core Concept

Atomic types are special data types that allow atomic operations on them, ensuring that the entire operation is executed as a single indivisible step. This prevents data races by making it impossible for other threads to access the variable during the operation. In C, atomic types were introduced with the C11 standard and are defined in the `` header file.

Syntax

The syntax for using atomic types is as follows:

_Atomic ( type-name ) // Declare a new atomic type
_Atomic type-name // Use as a type qualifier to designate the atomic version of an existing type

Here, type-name can be any data type except for arrays or functions. Note that that when used as a type specifier, type-name cannot be atomic or const-qualified. The header ` defines many convenience type aliases like atomic_bool, atomic_int, and atomic_uintmax_t`, which simplify the use of atomic types with built-in and library types.

Explanation

Objects of atomic types are free from data races, meaning they can be modified by two threads concurrently or read by one while being modified by another without causing inconsistencies. Each atomic object has its own associated modification order, a total order of modifications made to that object. If, from some thread's point of view, modification A of an atomic M happens-before modification B of the same atomic M, then in the modification order of M, A occurs before B.

It's essential to understand that although each atomic object has its own modification order, there is no single total order. Different threads may observe modifications to different atomic objects in different orders.

Notes

  • Atomic types can be mixed with const, volatile, and restrict qualifiers, but unlike other qualifiers, the atomic version of a type may have a different size, alignment, and object representation.
  • If the macro constant __STDC_NO_ATOMICS__ is defined by the compiler, the keyword _Atomic is not provided.
  • Atomic operations are usually more expensive than regular operations due to the additional synchronization overhead. Use atomic types judiciously and only when necessary for data consistency.

Worked Example

Let's consider a simple example of using atomic integers in a concurrent program:

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>

_Atomic int counter = 0;

void* incrementCounter(void* arg) {
for (int i = 0; i < 1000000; ++i) {
_Atomic int local = counter; // Read the value of counter atomically
local++; // Increment the value locally
_Atomic int success = compare_exchange_strong(&counter, local, counter);
if (!success || counter != local - 1) {
printf("Error: Data race detected\n");
return NULL;
}
}
return NULL;
}

int main() {
pthread_t threads[2];
for (int i = 0; i < 2; ++i) {
pthread_create(&threads[i], NULL, &incrementCounter, NULL);
}
for (int i = 0; i < 2; ++i) {
pthread_join(threads[i], NULL);
}
printf("Final counter value: %d\n", counter);
return 0;
}

In this example, we have a shared atomic integer counter that is incremented by two separate threads concurrently. The incrementCounter() function reads the current value of counter, increments it locally, and then attempts to update the shared variable with the new value using the compare_exchange_strong() function. If the update fails (indicating that another thread has modified the variable in the meantime), an error message is printed, and the program terminates.

Common Mistakes

  1. Not initializing atomic variables: Atomic variables must be initialized before use to avoid undefined behavior.
  2. Incorrect usage of atomic operations: Using atomic operations incorrectly can lead to data races or performance issues. Make sure to understand the semantics of each operation and use them appropriately.
  3. Ignoring errors during atomic updates: Failing to handle errors during atomic updates can cause the program to crash or behave unexpectedly. Always check for success after an atomic update and take appropriate action if it fails.
  4. Overusing atomic types: Atomic types should be used judiciously and only when necessary for data consistency. Overusing them can lead to performance issues due to the additional synchronization overhead.
  5. Not understanding memory models and data races: A proper understanding of memory models and data races is essential for using atomic types effectively. Make sure you understand how they work and how they relate to concurrent programming in C.
  6. Misusing or forgetting to free memory allocated with malloc() or other dynamic allocation functions when using atomic variables.
  7. Not properly synchronizing access to non-atomic data that is shared between threads when using atomic types.
  8. Assuming that atomic operations are thread-safe without proper synchronization, leading to potential data races and inconsistencies.
  9. Incorrectly assuming that atomic operations are always faster than regular operations due to the additional synchronization overhead.
  10. Not considering the impact of atomic types on cache coherence and performance in multi-core systems.

Practice Questions

  1. Write a function that atomically adds two atomic integers a and b. The function should return the result as an atomic integer.
  2. Implement a concurrent producer-consumer example using atomic queues.
  3. Write a function that atomically swaps the values of two atomic integers a and b.
  4. Explain the difference between compare_exchange_strong() and compare_exchange_weak().
  5. What is the purpose of the _Atomic int success variable in the worked example, and why is it necessary?
  6. Discuss the trade-offs between using atomic types and locks for synchronization in multi-threaded programming.
  7. Explain how memory ordering affects atomic operations and provide examples of memory ordering models in C.
  8. How can you determine if your compiler supports the C11 `` header file?
  9. What are some common performance optimizations for atomic types, and when should they be applied?
  10. Discuss the role of hardware support in atomic operations and how it affects their performance.

FAQ

  1. Why are atomic types more expensive than regular operations? Atomic operations require additional synchronization overhead to ensure data consistency, which can lead to increased CPU usage and slower performance compared to regular operations.
  2. Can I use atomic types with custom data structures like linked lists or trees? Yes, you can use atomic types with custom data structures, but it may require careful consideration of the synchronization strategy and potential performance implications.
  3. What is the difference between _Atomic and atomic in C++? In C++, std::atomic is a more modern and feature-rich version of atomic types compared to the _Atomic keyword in C. The C++ standard library provides a richer set of functions for working with atomic types, including support for custom memory models and stronger guarantees about atomic operations' behavior.
  4. Can I use atomic types on arrays or functions? No, atomic types cannot be used on arrays or functions due to their complex memory layouts and the challenges associated with synchronizing access to them.
  5. What is the role of the compare_exchange_strong() function in the worked example? The compare_exchange_strong() function is used to atomically update the shared atomic integer counter with a new value only if the current value matches the expected one. This ensures that the update is performed atomically and prevents data races. If the update fails (indicating that another thread has modified the variable in the meantime), an error message is printed, and the program terminates.
  6. What are some common performance optimizations for atomic types, and when should they be applied? Some common performance optimizations for atomic types include using compare_exchange_weak() instead of compare_exchange_strong() when no strong memory ordering is required, minimizing the use of atomic operations by combining multiple updates into a single operation, and leveraging hardware-specific instructions like compare-and-swap (CAS) where available. These optimizations should be applied judiciously to balance data consistency with performance.
  7. **How can you determine if your compiler supports the C11 ` header file?** To check if your compiler supports the C11 header file, look for the macro constant __STDC_VERSION__ in your compiler's documentation or source code. If it is greater than or equal to 20110L (representing C11), then the compiler should support atomic types defined in `.
  8. What are some common pitfalls when working with atomic types? Some common pitfalls when working with atomic types include assuming that atomic operations are always faster than regular operations, not considering the impact of atomic types on cache coherence and performance in multi-core systems, and overusing atomic types without proper synchronization, leading to potential data races and inconsistencies.
  9. How does hardware support affect atomic operations? Hardware support can significantly impact the performance of atomic operations by providing optimized instructions like compare-and-swap (CAS) that are faster than software implementations. Modern CPUs often have specialized hardware for handling atomic operations, which can lead to improved performance and reduced synchronization overhead.
  10. What is the relationship between atomic types and memory ordering? Atomic types in C provide a way to manipulate variables concurrently without causing data races. The memory ordering of atomic operations defines the order in which these operations are executed with respect to each other, ensuring that the program's behavior remains consistent across different processors and memory architectures.