Data Race Consequences
Incorrect Results
int x = 5; // Shared variable x
int y = 2 * x + 3 // Calculate the value of y (1)
int z = 3 * x + 2 // Calculate the value of z (2)
-
Thread A evaluates y
- Thread B interleaves and changes the value of x to 6
- Thread A uses the new value of x to evaluate z
- The calculated values of y and z are inconsistent
-
This can even occur inside a statement
y = x * x + 2 * x + 1; // Another thread could change the value of x
Incorrect Program Flow
int x = 3;
if (x > 0) {
z = sqrt(x);
}
- Thread A checks that x > 0
- Thread B interleaves and changes the value of x to a negative number
- Thread A uses the new, negative value of x
- A runtime error occurs which "shouldn't be possible"
Torn Writes and Reads
- A "torn" write
- Can occur when writing data requires multiple operations
- Another writing thread interleaves between the write operations
Threads A and B write to a memory location 0x???? Thread A starts writing 0x1234 0x12?? Thread B interleaves and writes 0x4567 0x4567 Thread A continues writing 0x1234 0x4534 The memory location contains part of Thread A's number and part of Thread B's number
Improper Construction
-
Thread A creates a shared object
- It calls the class's constructor
-
Thread B interleaves
- Thread B also calls the class's constructor, at the same memory location
-
We have a torn write
- The object will be initialized with a mixture of values from each thread
// Vector of polymorphic objects
std::vector<Base *> vec;
// The Base constructor adds the object to the vector
Base::Base() {
vec.push_back(this);
}
// The Derived constructor calls the Base constructor first
// Then it initializes the Derived members
Derived::Derived() : Base(), ...
-
Thread A calls Derived's constructor
- Derived's constructor calls Base's constructor
- Base's constructor pushes the Base part of the object onto the vector
-
Thread B interleaves
- Thread B accesses the element in vec
- The Derived constructor has not completed
- Thread B will see a partially constructed object
-
Thread A initializes the Derived part of the object
Improper Destruction
- Destructor of reference-counted object
*ref_count--;
if (*ref_count == 0) {
delete p;
delete ref_count;
}
-
Thread A decrements the counter
- Thread B interleaves and decrements the counter
- Thread A checks the counter and may release the members
- Thread B checks the counter and may release the members
-
If ref_count was initially 2, p and ref_count could be deleted twice
-
If ref_count was initially 1, p and ref_count may not be deleted at all
Managing Data Races
- There are no "benign" data races
- All data races in C++ are potentially dangerous
- Can be difficult to detect and replicate
- Intermittent errors
- Sensitive to environment
- Often dependent on timing coincidences or system load
- The only good solution is to prevent data races from occurring
Shared Data
- Avoid sharing data between different threads
- If unavoidable, synchronize the threads
- Impose an ordering on how the threads access the shared data
- This has substantial costs
- Increased execution time
- Increased program complexity