When it comes to thread safety in multithreaded programming, many people’s first reaction islocks (mutex)π.
For example, with a counter, some threads are incrementing while others are decrementing.
To prevent multiple threads from operating simultaneously, we can use locks for protection.
But is using a lock for a counter a bit too costly?
Is there a more lightweight way? β Yes, it is<span><span>std::atomic</span></span>.
What is <span><span>std::atomic</span></span>β
In simple terms, <span><span>std::atomic<T></span></span> is an atomic type, which guarantees that read and write operations on the variable are indivisible. This means that in a multithreaded environment, there will be no “half-read” or “half-write” situations.
We can think of it as a variable that is intrinsically thread-safe, requiring no explicit locking.
What types can it be used onβ
- Integer types (
<span><span>int</span></span>,<span><span>long</span></span>,<span><span>uint64_t</span></span>β¦) - Pointers (
<span><span>T*</span></span>) - Other types that are trivially copyable (i.e., can be copied byte-for-byte, such as POD types)
Common Operations π
<span><span>std::atomic</span></span> provides several common operations:
<span><span>load()</span></span>: Read value<span><span>store()</span></span>: Write value<span><span>fetch_add(x)</span></span>/<span><span>fetch_sub(x)</span></span>: Add/Subtract<span><span>exchange(x)</span></span>: Replace value and return old value
These operations are atomic, making them more efficient than using locks.
Example β¨
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::atomic<int> value{0};
// 1. store and load
value.store(10);
std::cout << "Initial value: " << value.load() << std::endl;
// 2. fetch_add
int old = value.fetch_add(5); // old value = 10, new value = 15
std::cout << "After fetch_add(5): old = " << old << ", new = " << value.load() << std::endl;
// 3. fetch_sub
old = value.fetch_sub(3); // old value = 15, new value = 12
std::cout << "After fetch_sub(3): old = " << old << ", new = " << value.load() << std::endl;
// 4. exchange
old = value.exchange(42); // replace with 42
std::cout << "After exchange(42): old = " << old << ", new = " << value.load() << std::endl;
// 5. Multithreaded counter
std::atomic<int> counter{0};
auto worker = [&counter]() {
for (int i = 0; i < 1000; i++) {
counter.fetch_add(1);
}
};
std::vector<std::thread> threads;
for (int i = 0; i < 4; i++) {
threads.emplace_back(worker);
}
for (auto &t : threads) {
t.join();
}
std::cout << "Final counter (should be 4000): " << counter.load() << std::endl;
return 0;
}
After running, you will see:
Initial value: 10
After fetch_add(5): old = 10, new = 15
After fetch_sub(3): old = 15, new = 12
After exchange(42): old = 12, new = 42
Final counter (should be 4000): 4000
Further Reading π
Here we have only introduced the most common usages, but <span><span>std::atomic</span></span> also has some more powerful features:
- Compare and Swap (CAS):
<span><span>compare_exchange_weak</span></span>/<span><span>compare_exchange_strong</span></span>, the cornerstone of lock-free data structures. - Memory Order: Control the read and write order between different threads, such as
<span><span>memory_order_relaxed</span></span>,<span><span>memory_order_acquire</span></span>,<span><span>memory_order_release</span></span><code><span><span>.</span></span>
Conclusion π―
Next time you write a multithreaded counter or flag, don’t rush to lock it; try using <span><span>std::atomic</span></span>, it’s simple and efficient!π