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