concurrency
multithreading
thread safety
programming
C++

I've heard i isn't thread safe, is i thread-safe?

Master System Design with Codemia

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

Introduction

If i is a shared ordinary integer, neither i++ nor ++i is thread-safe. The difference between postfix and prefix increment is only about the value returned by the expression. It does not make either operation atomic, and it does not prevent a data race when multiple threads update the same variable without synchronization.

Why Both Forms Are Unsafe on a Shared Non-Atomic Variable

For a normal integer, an increment conceptually involves:

  • reading the current value
  • computing the incremented value
  • writing the new value back

That read-modify-write sequence is exactly what races when two threads do it at the same time.

A simple example is:

cpp
1int counter = 0;
2
3void worker() {
4    for (int k = 0; k < 100000; ++k) {
5        ++counter;
6    }
7}

If multiple threads run worker concurrently, the final result is undefined behavior in C++ because the code contains a data race.

The same problem exists with counter++.

Prefix Versus Postfix Only Changes the Return Value

The meaning difference is local to the expression:

  • '++i increments and yields the incremented value'
  • 'i++ increments and yields the old value'

For example:

cpp
int i = 5;
int a = ++i; // i becomes 6, a is 6
int b = i++; // b is 6, then i becomes 7

That distinction matters for expression semantics and sometimes performance in iterator-heavy code, but it does not magically add thread safety.

So the answer to the original question is direct: no, ++i is not a thread-safe replacement for i++ on shared state.

Use std::atomic for Atomic Increments

If the variable is meant to be updated concurrently without a mutex, use std::atomic.

cpp
1#include <atomic>
2#include <thread>
3#include <vector>
4#include <iostream>
5
6std::atomic<int> counter{0};
7
8void worker() {
9    for (int k = 0; k < 100000; ++k) {
10        ++counter;
11    }
12}
13
14int main() {
15    std::vector<std::thread> threads;
16
17    for (int i = 0; i < 4; ++i) {
18        threads.emplace_back(worker);
19    }
20
21    for (auto& t : threads) {
22        t.join();
23    }
24
25    std::cout << counter.load() << '\n';
26}

Here, both ++counter and counter++ are atomic operations because counter is an atomic type.

The prefix/postfix distinction still affects the returned value, but thread safety now comes from the atomic type, not from the operator spelling.

Mutexes Are Still a Good Option

If the increment is part of a larger critical section, a mutex is often the better design.

cpp
1#include <mutex>
2
3int counter = 0;
4std::mutex m;
5
6void worker() {
7    for (int k = 0; k < 100000; ++k) {
8        std::lock_guard<std::mutex> lock(m);
9        ++counter;
10    }
11}

Atomic integers are great for simple counters. Once the update needs to coordinate multiple shared values, a mutex is usually clearer.

volatile Does Not Fix This

A common misconception is that volatile makes shared increments safe. It does not. volatile affects certain optimization behavior, but it does not make a read-modify-write sequence atomic and does not establish the synchronization rules required for safe multithreaded access in normal C++ application code.

Common Pitfalls

  • Assuming prefix increment is thread-safe while postfix increment is not is incorrect. Both are unsafe on shared non-atomic variables.
  • Focusing on expression syntax instead of data-race rules misses the real issue, which is unsynchronized shared access.
  • Using volatile as a concurrency tool does not solve atomicity or synchronization.
  • Switching to std::atomic for a variable that participates in a larger multi-variable invariant may still be the wrong design; a mutex might be needed.
  • Testing on one machine and seeing the "right" final count does not prove the code is correct because data-race behavior is still undefined.

Summary

  • Neither i++ nor ++i is thread-safe for a shared ordinary integer.
  • The only difference between them is the value returned by the expression.
  • Thread safety comes from synchronization, such as std::atomic or a mutex, not from prefix versus postfix syntax.
  • Use std::atomic for simple concurrent counters.
  • Use a mutex when the increment is part of a larger critical section.

Course illustration
Course illustration

All Rights Reserved.