Let's look at concurrency on the hot path. Lock-free code sounds heroic. Sharing less often wins harder.
Lock-Free Does Not Mean Automatically Faster
A mutex can be the correct tool when contention is low and the code is not latency critical.
Lock-free code can be slower or less safe when it introduces:
- Spinning under contention
- Excessive atomic operations
- False sharing
- Complex memory reclamation
- ABA problems
- Weak-memory-order bugs
- Poor observability
For many HFT pipelines, the best architecture minimizes sharing instead of making sharing more clever. Clever atomics are not a substitute for clear ownership.
A common topology is single-producer, single-consumer:
Each stage owns its mutable state. Communication happens through bounded queues.
This gives you:
- Clear ownership
- Fewer locks
- More predictable synchronization
- Easier testing
- Better cache locality
A Single-Producer, Single-Consumer Ring Buffer
A bounded SPSC queue is a useful building block when exactly one producer thread and one consumer thread access it.
The basic synchronization idea is:
The producer's release store makes earlier writes visible before the consumer sees the updated write index. The consumer's acquire load completes that handshake.
Important limitations
This queue is not a multi-producer, multi-consumer queue.
It assumes:
- Exactly one producer
- Exactly one consumer
- Capacity is a power of two
Tis trivially copyable- Queue-full behavior is handled by the caller
- The producer does not overwrite unread elements
- The consumer does not read unpublished elements
One more detail: the alignas(64) on write_index_ and read_index_ is the false-sharing fix that matters. Aligning the whole buffer_ array does not by itself solve producer/consumer ownership contention on the indexes.
In real systems, benchmark and validate the queue under the target architecture and compiler. Correctness comes before nanoseconds.
False Sharing Can Destroy Throughput
False sharing is two neighbors slamming the same hallway door. The variables are different. The cache line is not.
For example:
If producer and consumer run on separate cores, each update can bounce cache-line ownership even though they touch different variables.
Separate hot, independently written data:
The cache-line size is often bytes on modern x86 systems, but this is hardware-dependent. C++ provides:
Where supported, it can express intent more clearly:
std::hardware_destructive_interference_size is not guaranteed to be usable on every target and toolchain. Prefer it when available, and keep a platform-configured cache-line constant (often ) as a fallback for portable builds.
Measure the effect. Padding raises memory use and can worsen cache utilization if applied indiscriminately.
The Blank Flag Hazard: `volatile` Is Not Synchronization
This is the Blank Flag Hazard: it looks like a signal between threads, and it is not:
volatile tells the compiler that reads and writes are observable side effects. It does not give you:
- Atomicity
- Mutual exclusion
- Memory ordering
- A happens-before relationship
- Correct publication between threads
Use atomics:
Producer:
Consumer:
This example is intentionally tiny. Real shared-state designs often need more than one flag and a carefully specified protocol.
volatile can still matter for memory-mapped I/O or specialized hardware interactions, but it is not a substitute for std::atomic.
Memory Ordering: Use the Weakest Correct Ordering
The C++ memory model provides several orderings. The most common for message passing are:
For an SPSC publication pattern:
And:
Using relaxed everywhere may work on one CPU architecture and fail on another. Using seq_cst everywhere may be correct but can impose unnecessary constraints.
The goal is not to collect weak memory orderings like trading cards. Use a clearly correct ordering, then measure only if the synchronization cost actually matters.
The Takeaway
Minimize sharing first. Use SPSC queues when they fit, pad hot counters apart, never use volatile as a lock, and treat memory ordering as a correctness tool before a speed hobby.
Practice: draw the producer/consumer ownership boundary for a three-stage pipeline and mark which indexes need release/acquire.