Avoiding contention by design for faster locks.

The Fastest Lock Is the One Nobody Waits for

I spent three years in high-frequency trading watching senior devs throw `std::mutex` at every problem like they were throwing handfuls of sand at a leaking dam. They thought they were solving concurrency issues, but they were actually just building a massive, invisible bottleneck that strangled the throughput of the entire engine. Most tutorials treat concurrency as a series of locks to be managed, but that’s a lie. If you aren’t avoiding contention by design from the very first line of your architecture, you aren’t writing high-performance code; you’re just writing a slow program that happens to run on multiple cores.

I’m not here to teach you how to use a semaphore or how to wrap your data in a fancy thread-safe container. Those are band-aids, and I have no interest in band-aids. Instead, I want to talk about the structural decisions that prevent threads from ever needing to fight over the same cache line in the first place. I’ll show you how to think about data ownership and memory layout so that your hardware actually does what you intended, rather than spending its cycles arbitrating a war you should have prevented at the design stage.

Table of Contents

The False Security of Optimistic vs Pessimistic Locking

The False Security of Optimistic vs Pessimistic Locking

Most developers treat the choice between optimistic vs pessimistic locking as a simple toggle switch in a configuration file. They think they’re being clever by switching to an optimistic model to “improve performance,” but they rarely account for the cost of failure. In a pessimistic setup, you pay the tax upfront with a mutex or a semaphore. It’s heavy, yes, but the cost is predictable. You know exactly where the bottleneck is because the thread is literally waiting.

The danger with optimistic concurrency control is that the cost is deferred. You spend CPU cycles performing work, only to have the transaction fail at the final commit stage because someone else touched the same memory. If your contention is high, you aren’t actually gaining speed; you’re just burning cycles in a loop of wasted computation. You’ve traded a clean, blocked state for a chaotic cycle of retries that can tank your throughput faster than a heavy-handed lock ever could. It’s a false sense of progress that hides the underlying architectural friction.

Why Your Concurrency Control Mechanisms Are Failing You

Why Your Concurrency Control Mechanisms Are Failing You

The problem isn’t that your concurrency control mechanisms are broken; it’s that they are working exactly as intended, just not how you imagined. Most developers treat a mutex like a magic shield, assuming that if they wrap a critical section in a lock, the performance issues will simply vanish. In reality, you’re often just trading one type of latency for another. You move from a CPU spinning on a failed CAS loop to a thread being descheduled by the kernel, which is a massive context-switch penalty that most people completely ignore until the tail latency spikes.

We see this constantly when teams try to scale by throwing more cores at the problem. They implement complex lock-free data structures to bypass the overhead, only to find that they’ve merely shifted the bottleneck to the memory bus. You haven’t solved the contention; you’ve just turned a software problem into a hardware cache-coherency nightmare. If your design doesn’t account for how the underlying architecture actually moves data between cores, you aren’t building a high-performance system—you’re just building a more expensive way to wait.

Five Ways to Stop Fighting Your Own Threads

  • Stop treating every shared variable like a global necessity. If you can’t pass it by value or move it through a pipeline, you probably shouldn’t be sharing it. The best way to fix contention is to make it impossible for two threads to touch the same memory.
  • Design for data locality, not just thread safety. If your threads are constantly bouncing a single cache line between cores because of false sharing, your mutexes won’t save you from the latency penalty. Align your data to cache lines and give each thread its own workspace.
  • Favor message passing over shared state. Instead of protecting a complex object with a heavy-duty lock, send the work to the thread that owns the data. It turns a synchronization problem into a queueing problem, which is much easier to reason about.
  • Use fine-grained partitioning instead of monolithic locks. If you have a massive hash map, don’t lock the whole thing. Shard it. If you can’t access the entire structure at once, you shouldn’t be locking the entire structure.
  • Embrace lock-free primitives only when you actually understand the memory model. `std::atomic` is a scalpel, not a sledgehammer. If you use it to build a complex data structure without accounting for acquire/release semantics, you aren’t writing high-performance code; you’re just writing a very difficult bug.

The Hard Truths

Stop treating locks like a magic fix; if your data model is inherently contested, no amount of mutex tuning will save your throughput.

Optimistic concurrency isn’t a free lunch—if your write frequency is high, the cost of repeated retries will dwarf the overhead of a simple pessimistic lock.

True scalability comes from designing data structures that minimize shared state in the first place, rather than trying to manage the chaos after the fact.

Stop Fighting the Hardware

We’ve spent this time dissecting why your mutexes and atomic loops aren’t the silver bullets you hoped they were. The reality is that contention isn’t just a software problem; it’s a physical one. You can’t simply “code your way out” of cache line bouncing or the architectural tax of a poorly distributed data model. If your design forces multiple cores to fight over the same memory address, no amount of clever lock-free trickery will save your throughput. You have to stop treating concurrency as a layer you wrap around your logic and start treating it as the fundamental constraint that dictates how your logic must be structured.

My advice is simple: stop trying to manage contention and start designing it out of existence. Move toward data locality, embrace thread-local storage where possible, and partition your state so that cores rarely have a reason to talk to each other in the first place. C++ gives you the tools to control the machine with surgical precision, but that precision is wasted if you’re just building faster ways to collide. Build systems that respect the topology of the hardware, and the performance will follow. Otherwise, you’re just writing very expensive ways to wait.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

constexpr and compile time evaluation concept

Some of Your Program Can Finish Before It Starts

Code example for unit testing with Catch2.

A Test That Needs a Comment to Explain It Is Testing Too Much