Atomic bool fails to protect non-atomic counter
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
An atomic boolean can safely represent one boolean value, but it does not magically make a separate non-atomic variable safe. That is the core mistake behind designs where a std::atomic<bool> is used as a “guard” for a plain counter. If multiple threads can still read or write the counter without proper synchronization, the program has a data race and its behavior is undefined.
The important rule is that atomicity does not spread from one variable to another. You must synchronize the shared state itself, either by making the counter atomic or by protecting all accesses with a real lock.
Why the Atomic Flag Is Not Enough
Consider this kind of code:
At first glance, this looks like it should stop two threads from incrementing counter at the same time. It does not.
The problem is that the sequence “check flag, set flag, increment counter, clear flag” is not one indivisible operation. Two threads can both observe busy == false before either one stores true. That already breaks the intended mutual exclusion.
Even worse, counter is still a plain int, so unsynchronized concurrent access to it is a data race.
Use an Atomic Counter for Atomic Counting
If the job is just counting events, the simplest fix is to make the counter itself atomic.
This is the right solution when you only need atomic increments and reads. A dedicated atomic counter is simpler and safer than inventing a homegrown guard.
Use a Mutex for a Larger Critical Section
If the counter update is part of a bigger critical section, use a mutex.
A mutex protects the critical section as a unit. That is what developers often mean when they try to use an atomic boolean as a guard.
If You Really Want a Spin Lock, Use Atomic Operations Correctly
An atomic boolean can be used to build a spin lock, but only if you use read-modify-write operations such as exchange or compare_exchange. Even then, a standard mutex is often the better choice.
This actually serializes access to counter, because exchange changes the flag atomically while returning the previous value.
Still, writing your own spin lock is usually not the first choice. It can waste CPU and is easy to get wrong.
Memory Order Does Not Rescue a Bad Design
Developers sometimes hope that a stronger memory order will fix the original problem. It will not. Memory ordering matters for visibility and reordering, but it does not turn a non-atomic variable into a safely synchronized one.
If the shared counter is accessed concurrently without a proper happens-before relationship, the program is still broken no matter how carefully the boolean flag is loaded and stored.
Common Pitfalls
The biggest mistake is assuming “atomic somewhere” means “thread-safe everywhere.” Atomicity applies only to the operation and object involved.
Another issue is using load followed by store as though that were the same as an atomic test-and-set. It is not.
A third problem is building a custom spin lock when a plain std::mutex would be clearer, fairer, and less error-prone.
Summary
- An atomic boolean does not automatically protect a separate non-atomic counter.
- '
loadplusstoreis not a safe lock acquisition sequence.' - Use
std::atomic<int>when you only need an atomic counter. - Use
std::mutexwhen the counter update belongs to a broader critical section. - If you build a spin lock with
std::atomic<bool>, useexchangeorcompare_exchangecorrectly and do so only with clear intent.

