C++
atomic operations
concurrency
multithreading
duplicate-post

Are C Reads and Writes of an int Atomic?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C++, plain reads and writes of an int are not something you should treat as safely atomic for multithreaded code. A particular machine may happen to perform aligned integer loads and stores atomically at the hardware level, but the C++ memory model still says that unsynchronized concurrent access to a non-atomic variable is a data race and therefore undefined behavior.

What "Atomic" Means in Practice

An atomic operation is one that appears indivisible to other threads. No thread can observe a torn or partially updated value, and the language defines how that operation interacts with other memory operations.

That second point matters. Even if your hardware performs a read or write in one machine instruction, the language-level synchronization guarantees still do not exist unless you use atomic types or other proper synchronization tools.

Why Plain int Is Not Enough

This code has a data race:

cpp
1#include <thread>
2#include <iostream>
3
4int shared_value = 0;
5
6void writer() {
7    shared_value = 42;
8}
9
10void reader() {
11    std::cout << shared_value << '\n';
12}
13
14int main() {
15    std::thread t1(writer);
16    std::thread t2(reader);
17    t1.join();
18    t2.join();
19}

Even though int is a simple built-in type, this is still undefined behavior because one thread writes while another reads with no synchronization.

The problem is not just torn reads. The problem is that the C++ language does not define the interaction safely.

Use std::atomic<int> Instead

If you need concurrent reads and writes to an integer, use an atomic type.

cpp
1#include <atomic>
2#include <thread>
3#include <iostream>
4
5std::atomic<int> shared_value{0};
6
7void writer() {
8    shared_value.store(42, std::memory_order_relaxed);
9}
10
11void reader() {
12    std::cout << shared_value.load(std::memory_order_relaxed) << '\n';
13}
14
15int main() {
16    std::thread t1(writer);
17    std::thread t2(reader);
18    t1.join();
19    t2.join();
20}

Now the read and write are defined as atomic operations. Whether you want relaxed, acquire, release, or stronger ordering depends on the rest of the program, but the fundamental data-race problem is gone.

Hardware Reality Versus Language Guarantee

On many platforms, an aligned 32-bit int read or write is physically atomic. That fact often leads to misleading advice such as "plain int is fine on modern CPUs."

The problem with that advice is that portable C++ code must follow the C++ memory model, not rely on lucky hardware behavior. Compilers are allowed to optimize aggressively around non-atomic shared variables, and those optimizations are one reason undefined behavior matters here.

So the right answer is:

  • hardware may make it look atomic sometimes
  • C++ still does not let you rely on that in racy code

Atomic Read and Write Are Not the Same as Atomic Update

Even if individual loads and stores are atomic, compound operations are not automatically atomic.

cpp
1#include <atomic>
2
3std::atomic<int> counter{0};
4
5void increment() {
6    ++counter;
7}

This works because counter is atomic. But with a plain int, ++counter is definitely not safe across threads because it is a read-modify-write sequence, not one indivisible action.

That distinction matters a lot when people casually ask whether integer access is atomic. The practical code usually needs more than a single standalone load or store.

Common Pitfalls

One common mistake is equating "works on my machine" with "defined by the language." Concurrency bugs often pass tests until an optimization level, compiler, or architecture changes.

Another is assuming a simple built-in type is safe just because it fits in one register. C++ cares about synchronization semantics, not only hardware width.

Developers also sometimes use volatile for thread safety. In C++, volatile is not a substitute for std::atomic and does not fix data races.

Finally, do not forget that atomicity and ordering are different concerns. A variable can be atomic but still use the wrong memory order for the higher-level protocol.

Summary

  • Plain int access is not safely atomic in the sense required by the C++ memory model.
  • Unsynchronized concurrent reads and writes on a non-atomic int are a data race and undefined behavior.
  • Hardware behavior does not replace language-level guarantees.
  • Use std::atomic<int> for shared integer state accessed across threads.
  • Compound operations such as increment need atomic types or other synchronization too.

Course illustration
Course illustration

All Rights Reserved.