liveBook · Manning

Data race

As explained in C++ Concurrency in Action, Second Edition

A data race occurs when concurrent operations access the same memory location, at least one operation modifies it, and the operations are not properly synchronized.

A data race occurs when threads access the same memory location without an enforced ordering, at least one access is a write, and at least one access is non-atomic. It causes undefined behavior. A race condition is broader: it can arise when operations interleave in a way that breaks an invariant, even when it is not a data race. section 5.1.2 section 3.1.1

void undefined_behaviour_with_double_checked_locking()
{
    if(!resource_ptr)                                  #1
    {
        std::lock_guard<std::mutex> lk(resource_mutex);
        if(!resource_ptr)                              #2
        {
            resource_ptr.reset(new some_resource);     #3
        }
    }
    resource_ptr->do_something();                      #4
}


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":2,\"ch\":55},{\"line\":2,\"ch\":57}],[{\"line\":5,\"ch\":55},{\"line\":5,\"ch\":57}],[{\"line\":7,\"ch\":55},{\"line\":7,\"ch\":57}],[{\"line\":10,\"ch\":55},{\"line\":10,\"ch\":57}]]"}
!@%STYLE%@!

The first pointer check is outside the mutex. A thread can therefore read the pointer without synchronization with another thread’s initialization, risking a data race on both the pointer and the pointed-to object. section 3.3.1

Protect shared access with synchronization, or use atomic operations where appropriate. Atomic access can make behavior defined, but does not prevent competing operations or specify which occurs first. For one-time initialization, std::call_once provides synchronization between initialization and its callers. section 5.1.2 section 3.3.1

std::for_each(std::execution::par,v.begin(),v.end(),
    [&](int& x){ x=++count; });


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":14},{\"line\":0,\"ch\":33}]]"}
!@%STYLE%@!

This parallel algorithm increments a shared count from multiple lambda invocations. That unsynchronized modification creates a data race; replacing count with an atomic or using a mutex can make access defined, though synchronization may serialize the calls. section 10.2.3

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Defines the access and ordering conditions, explains undefined behavior, and illustrates hazards without developing the full memory model.
Examples
Uses double-checked resource initialization and a parallel lambda that modifies a shared counter.
What you can do after
Evaluate whether concurrent accesses require ordering and whether atomic operations or synchronization make them defined.
How it connects
Connects data races to synchronization, atomic operations, mutexes, lazy initialization, and parallel algorithms.

Also explained in

Compare how 4 books explain data race →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.