C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
113: Data Races and How to Avoid Them
A few years ago, I worked with a developer who spent three straight days hunting a "ghost" in a telemetry system. On his four-core laptop, the code worked perfectly. But the moment it was deployed to a 64-core production server, the total request counts were consistently wrong—sometimes off by a few hundred, sometimes by thousands. He kept restarting the service, thinking it was a network glitch or a memory leak. It wasn't. He had a classic data race. He was incrementing a shared integer from multiple threads without any synchronization, and on the beefier server, the threads were overlapping in just the right way to wipe out each other's work.
In C++, a data race occurs when two or more threads access the same memory location concurrently, at least one of those accesses is a write, and the threads aren't using any synchronization to coordinate. Here is the kicker: under the C++ memory model, a data race is undefined behavior. This doesn't just mean you get a wrong number; it means the compiler is allowed to assume a race never happens, which can lead to optimizations that literally delete your logic or cause crashes that are nearly impossible to reproduce in a debugger.
The Read-Modify-Write Trap
To understand why this happens, you have to stop thinking of count++ as a single action. To the CPU, that one line of C++ is actually three distinct steps: load the value from memory into a register, increment the register, and store the value back into memory.
// What you think happens:
count = count + 1;
// What actually happens at the machine level:
Register A = load(count); // Thread 1 loads 10
Register B = load(count); // Thread 2 loads 10
Register A = Register A + 1; // Thread 1 increments to 11
Register B = Register B + 1; // Thread 2 increments to 11
store(count, Register A); // count is now 11
store(count, Register B); // count is still 11 (one update is lost!)
This is why the bug was so elusive for my colleague. On a slow machine, the threads might not overlap this way often. But on a high-core machine, the probability of two threads hitting that load-store window at the same time skyrockets.
Shielding Data with Mutexes
The most common way to stop this is by using a std::mutex (short for mutual exclusion). Think of a mutex as a physical key to a room. If you want to touch the shared data, you have to hold the key. If someone else has it, you wait in line until they put it back.
I strongly advise against calling .lock() and .unlock() manually. If your code throws an exception or returns early between those two calls, the mutex stays locked forever, and your program deadlocks. Instead, use std::lock_guard or std::unique_lock. These use RAII to ensure the mutex is released the moment the guard goes out of scope.
#include <mutex>
int shared_counter = 0;
std::mutex counter_mutex;
void increment() {
// The mutex is locked here. It will unlock automatically at the end of the function.
std::lock_guard<std::mutex> lock(counter_mutex);
shared_counter++;
}
When to Reach for Atomics
Mutexes are powerful, but they are heavy. Locking a mutex involves a system call that can put your thread to sleep, which is a massive performance hit if you're just incrementing a number. For simple types, C++ provides std::atomic.
Atomic operations are implemented using special CPU instructions (like "compare-and-swap") that guarantee the read-modify-write cycle happens as a single, indivisible unit. No other thread can "sneak in" halfway through. If you're just managing a counter or a boolean flag, std::atomic is almost always the better choice.
#include <atomic>
// No mutex needed here
std::atomic<int> shared_counter{0};
void increment() {
shared_counter++; // This is now a single, thread-safe atomic operation
}
📋 Practical Task
Fixing the Concurrent Packet Processor
You have been handed a piece of code for a network packet processor. It is supposed to count how many packets were successfully processed across multiple worker threads. However, the total_packets_processed count is consistently lower than the actual number of packets sent because of a data race.
Your Task: Modify the provided code to eliminate the data race. You may choose to use either a std::mutex with a std::lock_guard or std::atomic. Given that this is a high-performance packet processor, consider which one is more appropriate for a simple counter.
#include <iostream>
#include <vector>
#include <thread>
int total_packets_processed = 0; // This is the race condition source
void process_packets(int packets_per_thread) {
for (int i = 0; i < packets_per_thread; ++i) {
// Simulate some work
total_packets_processed++;
}
}
int main() {
const int num_threads = 8;
const int packets_per_thread = 100000;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(process_packets, packets_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout < "Expected: " < (num_threads * packets_per_thread) < std::endl;
std::cout < "Actual: " < total_packets_processed < std::endl;
return 0;
}
There are no comments for now.