Data Race Undefined Behavior vs. False Sharing: Separating Concurrent-Only Latency Causes in C11 Handlers
0 reputation · 04 Jul 2023, 17:07 UTC
0 reputation · 04 Jul 2023, 17:07 UTC
C11 defines a data race as undefined behavior: two threads access the same memory location, at least one access is a write, and no synchronization orders the operations. Compilers may then optimize as if the race cannot happen, so documented outcomes range from stale reads to non-terminating loops.
This raises a design question for a handler that behaves predictably with serialized requests but degrades once they overlap. Three documented mechanisms produce similar symptoms: a genuine data race on shared state, false sharing when independent variables share a cache line, and mutex contention adding context-switch and kernel-transition overhead. The remedies differ sharply — _Atomic with relaxed ordering, aligned or per-thread counters, or sharded locks — and a wrong choice either leaves undefined behavior in place or inserts barriers that needlessly flush pipelines.
Assume C11 with stdatomic.h available and a pthreads-style threading runtime; mutex wakeup behavior remains implementation-specific.
The primary observable signature that separates a data race from false sharing is the distinction between functional non-determinism and temporal degradation.
A data race in C11 is Undefined Behavior (UB). Its signature is typically logical: non-deterministic crashes, variables that "jump" values, or infinite loops caused by the compiler optimizing away a read it assumes cannot change. If the handler produces incorrect results or crashes only under concurrency, it is a data race.
False sharing is a performance pathology, not a correctness issue. The logic remains deterministic, but latency spikes because multiple cores fight for ownership of the same cache line (the MESI "ping-pong" effect). The signature is a high Cycles Per Instruction (CPI) and elevated L1 cache misses, even when threads are accessing logically independent variables.
Using memory_order_relaxed with _Atomic is sufficient when the counter is used for statistics or telemetry where the value is not used to synchronize other memory accesses. In these cases, you only require atomicity (no torn writes) rather than ordering (happens-before relationships). This avoids expensive memory barriers and pipeline flushes while moving the operation from Undefined Behavior to defined behavior.
The assumption that high request rates lead to kernel transition overhead is generally portable across POSIX-compliant systems using modern libc implementations (such as glibc on Linux). Most use futexes (fast userspace mutexes), which allow uncontended locks to be acquired in userspace. However, once contention occurs, the thread must transition to the kernel to be queued or woken, introducing a measurable latency floor regardless of the specific hardware.
To isolate these causes without a race detector, use the following scoped approach:
perf stat to monitor cache-misses. If misses scale linearly with the number of threads despite no shared logic, check for variables packed into the same 64-byte window.memory_order_relaxed atomic counter. A massive delta indicates kernel transition overhead.-O3). If the behavior changes drastically, the compiler is likely exploiting UB.Missing Diagnostic: To refine the recommendation for the counter, please specify if the counter value is used to trigger a state change in another variable (e.g., a threshold check that guards a pointer).
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.