C Thread Safe Integer
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When multiple threads update the same integer in C++, a plain int is not enough. Reads and writes can interleave in undefined ways, producing lost updates, stale values, and race conditions that are often difficult to reproduce.
The usual answer is either std::atomic<int> or a mutex around a normal integer. The correct choice depends on whether you are protecting a single value or a larger invariant that spans several operations.
Use std::atomic<int> for Simple Shared Counters
For counters, flags, and single-value state transitions, std::atomic<int> is the standard tool. It guarantees that each operation is atomic and that the compiler will not optimize it into unsafe code.
The call to fetch_add ensures that no increments are lost. Without the atomic type, two threads could read the same old value and both write back the same new value.
Understand What Atomic Does and Does Not Protect
Atomic operations protect the integer itself, not the meaning around it. If your logic says "increment only if another variable has a certain value", then the whole decision must be synchronized, not just the increment.
Consider this pattern:
That code is still racy at the logical level because another thread can change remaining between the load and the subtraction. In that case, use compare_exchange or a mutex.
This loop retries until it either successfully updates the value or discovers that no slots remain.
Use a Mutex for Compound Invariants
If the integer is part of a larger state update, a mutex is clearer and safer. Mutexes protect critical sections rather than individual machine operations.
Here, both balance and version must move together. Using an atomic integer for only one field would not keep the pair consistent.
Choosing Memory Ordering
Many examples use the default sequentially consistent ordering because it is easiest to reason about. For simple counters where you only care about accurate totals, std::memory_order_relaxed is often enough and can reduce overhead.
Use stronger ordering when the integer coordinates access to other shared data. For example, a published state flag may need release and acquire semantics:
The release and acquire pair ensures that once reader sees ready == 1, it also sees the initialized payload.
Common Pitfalls
- Treating
volatile intas a thread-safety mechanism. In C++,volatileis not a substitute for atomic or mutex-based synchronization. - Mixing atomic and non-atomic access to the same variable. Every access must follow the same synchronization strategy.
- Assuming atomic operations protect related data structures. They do not preserve wider invariants by themselves.
- Overusing lock-free techniques for code that would be simpler with a mutex. Correctness is more important than theoretical speed.
- Choosing relaxed memory ordering without understanding whether the integer coordinates access to other data.
Summary
- Use
std::atomic<int>for simple counters, flags, and single shared integers. - Use
compare_exchangewhen an update depends on the current value. - Use a mutex when the integer is part of a larger state transition.
- '
volatiledoes not make integer access thread-safe in C++.' - Pick memory ordering deliberately, with sequential consistency as the safest default.

