Data Races
Understand how data races occur in concurrent C++ programs when multiple threads access shared memory simultaneously with at least one modifying it. Learn the risks of undefined behavior and explore practical techniques like atomic types and mutex locks to avoid data races. This lesson also covers concepts such as critical sections, contention, deadlocks, and the difference between synchronous and asynchronous tasks, helping you write safer and more reliable multithreaded code.
We'll cover the following...
A data race happens when two threads are accessing the same memory simultaneously, and at least one of the threads is mutating the data. If our program has a data race, it means that our program has undefined behavior. The compiler and optimizer will assume that there are no data races in our code and optimize it under that assumption. This may result in crashes or other completely surprising behavior. In other words, we can under no circumstances allow data races in our program. The compiler usually doesn’t warn us about data races since they are hard to detect at compile time.
Note: Debugging data races can be a real challenge and sometimes requires tools such as ThreadSanitizer (from Clang) or Concurrency Visualizer (a Visual Studio extension). These tools typically instrument the code so a runtime library can detect, warn about, or visualize potential data races while running the program we are debugging.
Example: A data race
The diagram below shows two threads that are going to update an integer called counter. Imagine that these threads are both incrementing a global counter variable with the instruction ++counter. It turns out that incrementing an int might involve multiple CPU instructions. This can be done in different ways on different CPUs, but let’s pretend that ++counter generates the following made-up machine instructions:
- R: Read counter from memory
- +1: Increment counter
- W: Write new counter value to memory
Now, if we have ...