Timing as a hidden assumption

Concurrent execution allows multiple flows of work to interleave. A race becomes a correctness problem when some permitted orderings satisfy the program's requirements while others violate them. Instructions need not run simultaneously on separate processors: switching between threads on one processor can expose the same flaw.

Not every variation in completion order is an error. Two independent tasks may legitimately finish in either order. The relevant question is whether an invariant or required dependency is broken, such as preserving every increment or initializing an object before using it. Identical output across repeated runs is not the definition of correctness.

References: [1] [2]

A lost-update example

Suppose a shared counter starts at ten and two threads each increment it once. Model an increment as separate read, calculate, and write steps. Both threads can read ten before either stores a result; each then writes eleven. Two intended increments have produced only one net increase, whereas two correctly serialized increments would leave twelve.

The example shows why a whole read-modify-write operation may require protection. A single source-code statement is not necessarily indivisible. There is an important language caveat: an actual C++ program with a data race on an ordinary variable has undefined behavior. This simplified interleaving illustrates lost updates; it does not enumerate all outcomes the language permits for that faulty program.

References: [1] [3]

The narrower meaning of data race

The C++ memory model defines a data race through potentially concurrent conflicting actions, at least one non-atomic, with neither happening before the other, subject to its specified exceptions. Conflicts also encompass certain object-lifetime operations. For ordinary cross-thread access, observed wall-clock order is not a substitute for the required synchronization relation.

A broader logical race can remain without a data race. Consider separate atomic loads and stores used to implement the counter increment. Both threads may still load ten and later store eleven. Each individual access can be atomic while their composition is not an atomic read-modify-write operation. Data-race freedom therefore does not establish correctness of the whole algorithm.

References: [3] [1]

Making the requirement explicit

A lost update can be prevented by consistently protecting the entire update with a common lock, or by using an appropriate atomic read-modify-write operation. For a check followed by an action based on that check, the protection must cover the relevant compound operation. Protecting only the final write can leave the earlier decision invalidated.

Other bugs violate a required order rather than merely allowing overlapping updates: a worker may use an object before initialization completes. Such a dependency needs an explicit synchronization protocol, not an assumption about which thread usually runs faster. Locks, condition waiting, and atomic operations address different needs. The essential repair is to replace an accidental timing assumption with an enforced program relationship.

References: [2] [4] [3]