A producer thread fills a response object and then sets ready = true. A consumer waits until ready becomes true and reads the response.
The intended order seems obvious:
The producer writes the response and then publishes ready. The consumer observes ready and then reads the response.
Making ready atomic prevents that flag from tearing. Whether it also publishes the response depends on the ordering semantics used for the store and load; atomicity alone does not answer whether the response writes must become observable before the flag or whether the consumer's response reads must remain after its flag check.
This is a memory-ordering problem. Modern compilers and processors reorder work to improve performance, so concurrent code needs explicit rules connecting operations across threads.
The same hardware creates a second, less obvious problem. Two threads can update completely independent variables and still slow each other down when those variables occupy the same cache line. That performance failure is called false sharing.
Source code gives each thread a program order:
A person reading the function sees the payload assignment first and the ready assignment second. Three different orders still matter:
These orders do not always match.
A compiler may move independent operations when doing so does not change the behavior of a valid single-threaded execution. A processor may issue loads early, execute independent instructions out of order, and temporarily hold stores in internal buffers.
These optimizations are essential. Waiting for every memory access to finish globally before beginning the next instruction would waste much of a modern CPU's execution capacity.
The single-threaded result must still appear correct to that thread. The difference becomes visible when another execution path observes shared memory.
Memory ordering defines which observations concurrent execution paths are allowed to make.
Stores are often placed in a per-core store buffer before they become visible to other cores. This allows the writing core to continue instead of waiting for the cache-coherence work to complete.
Consider two atomic variables initially set to zero:
Two threads use relaxed atomic operations:
| Thread A | Thread B |
|---|---|
store x = 1 | store y = 1 |
load y into observed_by_a | load x into observed_by_b |
An execution may behave like this:
| Step | Core A | Core B |
|---|---|---|
| 1 | buffer store x = 1 | buffer store y = 1 |
| 2 | load y sees 0 | load x sees 0 |
| 3 | publish x = 1 | publish y = 1 |
Both loads read zero even though both stores were issued first, because neither store had been published when the loads ran.
Both threads can observe zero even though each thread placed its store before its load in source order.
The CPU does not need to literally execute the source instructions backward. The stores can remain buffered while the later loads obtain values. From the perspective of the other core, the loads became effective before the stores were visible.
Using C11 relaxed atomics makes this example a defined program whose operations intentionally impose very little cross-location ordering:
Plain C integers would introduce data races and undefined behavior, preventing this example from isolating the hardware ordering issue.
Loading simulation...
Atomicity ensures that one operation on one object is indivisible. Ordering constrains how several operations may be observed relative to one another.
An atomic store to ready guarantees that a consumer does not read a torn flag. It does not automatically make preceding payload writes part of the same atomic event.
A memory-ordering constraint does the opposite kind of work. It can require the payload writes to become visible before publication, but it does not turn an ordinary shared increment into an atomic read-modify-write operation.
| Mechanism | What it guarantees |
|---|---|
| Atomic operation | The indivisibility of one supported transition |
| Ordering constraint | What can be observed across multiple operations |
Correct concurrent code often needs both properties, but they answer different questions:
Modern multiprocessors normally maintain cache coherence. If several cores cache the same memory location, the coherence protocol coordinates writes so that they do not keep permanently conflicting versions.
Coherence is primarily a per-location property. All cores must agree on a sensible order for writes to x.
That does not establish one universal order across different locations:
A memory consistency model, often shortened to memory model, defines which combinations of observations are legal across locations and threads.
The hardware architecture has a memory model. The programming language has one as well. The compiler must translate the language guarantees into instructions that remain correct under the hardware guarantees.
Portable application code should therefore use the language's synchronization operations. Assuming behavior from one CPU architecture can fail after recompilation for another architecture, and using a special instruction does not repair source code that violates the language memory model.
The processor is only one source of reordering. A compiler may:
For valid single-threaded code, these transformations preserve observable behavior under the language's as-if rule: the optimized program must behave as if it followed the abstract language rules.
Unsynchronized concurrent access to ordinary objects is not protected by those expectations in C. If a program has a data race, the compiler can make optimizations based on the fact that valid executions do not contain that race.
This is why inspecting generated assembly is not enough to prove thread safety. A particular compiler version may currently produce the expected order, but the source code still lacks a language-level guarantee.
Synchronization operations communicate constraints to both layers:
A relaxed atomic operation preserves the atomicity of that operation without establishing a general ordering relationship with other memory accesses.
For example:
Competing atomic increments cannot lose updates. All modifications of this atomic counter still have a well-defined order. The operation does not promise that unrelated data written before the increment has been published to a thread that later reads the counter.
Relaxed ordering is appropriate only when the value does not carry a message about other memory.
A statistics counter is a common example. If a thread only needs to count completed events, the increment may require atomicity but no relationship with the contents of those events.
A readiness flag is different. Its purpose is to announce that other data is ready. Treating it as a relaxed atomic flag omits the very relationship the flag is meant to communicate.
A release operation publishes earlier work. An acquire operation that observes that publication makes the earlier work available before the acquiring thread continues with dependent shared access.
The producer can initialize an ordinary payload and then perform a release store:
The consumer uses an acquire load:
When the acquire load reads the value published by the release store, the operations form a synchronization relationship:
The release-store and the acquire-load are the only synchronizing pair here. Everything the producer wrote before its release is visible to the consumer after its acquire.
The payload writes happen before the consumer's payload reads in the language memory model. The ordinary payload accesses are therefore valid under this one-time publication protocol.
The relationship is conditional: the acquire must observe the release or a value connected to it. A load that still sees false has not acquired the payload.
The producer must also stop modifying the payload while the consumer reads it. Release and acquire publish the completed state; they do not make future unsynchronized modifications safe.
Sequentially consistent atomic operations provide a model that is easier to reason about. All such operations participate in one global order that is consistent with the program order of each thread.
Conceptually:
The basic C atomic operations use sequential consistency by default:
Sequential consistency does not make an entire function atomic, and it does not legitimize data races on nearby non-atomic objects. It provides stronger ordering for participating atomic operations.
Stronger ordering can require extra compiler or hardware constraints on some architectures. The performance difference depends on the operation and target CPU; it should be measured rather than assumed.
Use the weakest ordering that is clearly correct only when the synchronization argument is understood. A slightly stronger operation is often preferable to an incorrect optimization whose effects appear only under rare schedules.
A memory barrier, or memory fence, constrains memory operations across a point.
At a simplified level:
A barrier separates the operations issued before it from those issued after it.
The exact constraint depends on the barrier. Some order loads, some order stores, and some order both. Architectures expose different instructions because their default memory models differ.
There are also two layers of barrier:
A compiler barrier alone cannot force another core to observe stores in the required order. A hardware barrier inserted into source code without compiler semantics can still be undermined by compiler movement. Correct language atomics let the compiler provide both parts as needed.
C exposes atomic_thread_fence for algorithms that require a standalone fence. Fences are easy to misuse because they do not identify the communicating object by themselves. Release and acquire operations on the publication variable usually express a simple handoff more directly.
A barrier is not a command to write every dirty cache line to RAM. Cache coherence and memory ordering are related but distinct mechanisms. A barrier enforces an ordering guarantee; the hardware may satisfy it through buffers, coherence messages, and architecture-specific instructions without flushing all caches.
Processor architectures permit different default reorderings.
x86-64 has a relatively strong memory model. Ordinary cached stores are observed in store order, but a later load from another address can become effective before an earlier store is globally visible, as in the store-buffer example.
Architectures such as AArch64 and RISC-V permit more kinds of reordering unless instructions carry ordering semantics or explicit barriers are used.
“Strong” does not mean “concurrent code needs no synchronization.” Compiler transformations still exist, store buffering still matters, atomic read-modify-write operations are still required for compound updates, and the language memory model still governs source validity.
Operating-system kernels use architecture-aware barrier interfaces because the same kernel source must run correctly on machines with different ordering rules. Application code receives the same portability benefit from language atomic and synchronization APIs.
Cache coherence operates at cache-line granularity, not variable granularity. A line is commonly 64 bytes on current general-purpose processors, although software must not assume that every machine uses that size.
Suppose two frequently updated counters are adjacent:
They may occupy one cache line:
The two counters are separate variables that no thread shares. The hardware does not track them separately, though, because ownership is granted for the whole line.
The threads do not access the same variable. Their updates are logically independent and can be completely race-free.
The hardware cannot transfer ownership of only the bytes containing first. When Core 0 obtains the line for writing, Core 1's copy of the whole line can be invalidated. Core 1 must then regain write ownership to update second, invalidating Core 0's copy.
The line bounces between the cores on every increment. Each transfer costs real time, and the program never asked for any sharing at all.
This repeated movement is often called cache-line bouncing.
False sharing occurs when threads modify different data that happens to share a cache line, causing unnecessary coherence traffic.
It is “false” because the program does not logically share the variables. The hardware sees sharing because its coherence unit is the containing cache line.
Loading simulation...
With true sharing, threads access the same data:
The coherence traffic reflects a real dependency. If every request must update one exact counter immediately, the conflicting writes are inherent in that design.
With false sharing, threads access distinct data:
The dependency is accidental and may be removed by changing the layout.
False sharing is usually a performance problem rather than a correctness problem. Both counters can end with the right values while execution takes much longer than expected.
This produces a characteristic symptom: adding threads increases CPU activity but provides little throughput improvement, even though code review finds no shared logical counter or obvious critical section.
One repair is to place independently written counters on separate cache lines:
Because array elements must preserve the alignment of their type, each padded_counter begins on a 64-byte boundary and its size includes enough trailing space for the next element to remain aligned on common C implementations that support this alignment.
Alignment alone is not always separation:
The structure begins at a line boundary, but second may still be placed immediately after first in the same line.
The hard-coded size is also platform-specific. Production code should obtain an appropriate cache-line or destructive-interference size from its supported platform, or isolate the layout decision behind a configuration boundary.
Other useful strategies are to:
Padding has a cost. It increases memory footprint and can reduce useful cache density. It is most valuable for frequently written data accessed concurrently by different cores, not as a blanket rule for every structure.
The following program compares two atomic counters stored next to each other with two counters aligned into separate 64-byte slots.
Relaxed atomic increments make every iteration a defined, indivisible update without publishing other data. They also prevent the compiler from replacing the entire loop with one final addition.
Compile and run it:
The exact times are machine- and run-dependent. If the threads execute on different cores and the assumed line size matches the machine, the separated layout will often be substantially faster. If both threads share one core, the difference may be small because the line is not bouncing between private caches.
Run the program several times and compare the pattern, not one measurement. CPU migration, frequency scaling, other system activity, and virtualized hardware can all add noise.
On a Linux system with supported performance-monitoring events, perf c2c can help attribute cache-to-cache traffic to contended lines:
Availability and required permissions vary. Timing shows that the layouts behave differently; hardware counters provide stronger evidence that cache-line movement is the cause.
False sharing is worth investigating when a workload has all of these characteristics:
Start with ownership and layout. Determine the address of each hot field, its offset within the containing structure, and which threads modify it. Two fields with different names or array indices can still occupy the same line.
Then measure. A benchmark should preserve the real write frequency and thread placement closely enough to reproduce the effect. Hardware performance counters or cache-to-cache analysis can help distinguish false sharing from ordinary cache misses, scheduling overhead, or true contention.
After changing the layout, verify both throughput and memory use. A faster microbenchmark is useful, but the production trade-off also includes a larger working set and the behavior of the complete workload.
Memory ordering defines which effects one execution path must observe after it observes an event from another. Atomicity protects one operation; ordering connects multiple operations. Cache coherence keeps writes to a location consistent, but it does not by itself publish related data across locations.
Relaxed atomics provide indivisible operations with minimal cross-location ordering. Release and acquire operations establish a publication relationship, while sequentially consistent atomics provide a stronger global ordering model. Language atomics allow the compiler to enforce these guarantees on architectures with different hardware memory models.
False sharing occurs when independent, frequently written variables occupy the same cache line. Their values remain correct, but the line bounces between cores and limits scalability. Separating hot writable data, measuring cache-to-cache traffic, and balancing padding against memory footprint can remove this accidental hardware contention.
5 quizzes