Atomic Operation & Memory Model in C++
What is atomic in C++?
atomic in C++?In C++, std::atomic is a template class provided by the C++ Standard Library (since C++11) that allows you to create variables that can be safely shared and manipulated across multiple threads without the need for explicit synchronization mechanisms like mutexes.
Key Characteristics of std::atomic:
std::atomic:Thread Safety: Operations on
std::atomicvariables are guaranteed to be atomic, meaning they complete as a single, indivisible step. This prevents race conditions where multiple threads might try to read, modify, or write to the same variable simultaneously.Lock-Free Operations: Many atomic operations are implemented without the use of locks, which can lead to better performance and avoid issues like deadlocks.
Memory Ordering:
std::atomicprovides various memory ordering options (likestd::memory_order_relaxed,std::memory_order_acquire, etc.) to control the visibility and ordering of operations across different threads. However, the default behavior (std::memory_order_seq_cst) ensures a strict ordering, which is often sufficient for many use cases.Specialized Types: C++ provides atomic versions of many fundamental types, such as
int,bool, pointers, etc. For more complex types, you might need to use other synchronization mechanisms.
How to Use std::atomic:
std::atomic:Here's a simple example:
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter(0);
void increment() {
for(int i = 0; i < 1000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}In this example:
Two threads increment the same
countervariable.Using
std::atomic<int>ensures that eachfetch_addoperation is atomic, preventing race conditions.The final printed value will reliably be
2000, demonstrating thread-safe increments.
What are Atomic Operations in C++?
Atomic operations are operations that are performed as a single, indivisible step. In the context of multithreading, atomic operations ensure that when multiple threads attempt to read or modify a shared variable, these operations do not interfere with each other, maintaining data consistency and preventing undefined behavior.
Common Atomic Operations:
Load and Store: Reading from and writing to an atomic variable.
int value = atomic_var.load(); atomic_var.store(10);Fetch and Add/Subtract: Atomically adding or subtracting a value.
atomic_var.fetch_add(1); atomic_var.fetch_sub(1);Compare and Exchange: Atomically compares the current value with an expected value and, if they match, replaces it with a new value.
int expected = 5; atomic_var.compare_exchange_strong(expected, 10);Exchange: Atomically replaces the current value with a new value and returns the old value.
int old = atomic_var.exchange(20);
These operations ensure that even if multiple threads attempt to perform these operations concurrently, each operation completes fully without being interrupted or causing inconsistent states.
Relating to the ZeroEvenOdd Class
ZeroEvenOdd ClassLet's examine how std::atomic and atomic operations are utilized in ZeroEvenOdd class:
class ZeroEvenOdd {
private:
int n;
atomic<int> flag = 0;
public:
ZeroEvenOdd(int n) {
this->n = n;
}
// Other member functions...
};Purpose of atomic<int> flag
atomic<int> flagShared State Management: The
flagvariable is used to control the execution flow between three different threads: one printing zeros, one printing even numbers, and one printing odd numbers. Since multiple threads access and modifyflag, it must be managed safely to prevent race conditions.Atomicity Ensured: By declaring
flagasstd::atomic<int>, you ensure that any read or write toflagis atomic. This means that when one thread updatesflag, other threads see the change immediately and consistently, without any partial updates or data races.Synchronization Mechanism: The
flagvariable acts as a simple synchronization mechanism:zeroFunction: Waits untilflagis0, prints0, and then setsflagto1(odd) or2(even) based on the current iteration.evenFunction: Waits untilflagis2, prints the even number, and resetsflagto0.oddFunction: Waits untilflagis1, prints the odd number, and resetsflagto0.
Why Not Use Non-Atomic Variables?
If flag were a regular int, the following issues could arise:
Race Conditions: Multiple threads might try to read and write to
flagsimultaneously, leading to inconsistent or unexpected behavior.Undefined Behavior: Concurrent modifications without synchronization can cause undefined behavior, making the program unreliable.
By using std::atomic<int>, you avoid these problems because all operations on flag are inherently thread-safe.
Example Flow:
Initial State:
flag = 0.zeroThread:Sees
flag == 0, prints0.Sets
flagto1(if the next number is odd) or2(if even).
oddorevenThread:Depending on
flag, either theoddthread or theeventhread proceeds.Prints the respective number and resets
flagto0, allowing thezerothread to print the next0.
This cycle continues until all numbers up to n are printed in the desired order.
Conclusion
std::atomicprovides a way to perform thread-safe operations on variables without explicit locks.Atomic operations ensure that read-modify-write sequences happen indivisibly, preventing race conditions.
In the
ZeroEvenOddclass,std::atomic<int> flagis crucial for coordinating between multiple threads to maintain the correct printing sequence.
Last updated