Designed for high-concurrency C++20 environments where throughput is critical, but thread starvation is unacceptable.
- Hybrid Two-Tier Locking combining an atomic uncontended fast path with a managed wait queue.
- Deterministic Fairness Escalation preventing starvation through explicit policy thresholds.
- Logical Baton Handoff explicitly identifying and notifying the next eligible waiter.
- Configurable Fairness Policy allowing starvation thresholds, writer batching, and escalation behavior to be tuned for different workloads.
- The Problem: Unpredictable Fairness and Starvation
- Key Architectural Highlights
- The Fairness State Machine
- Algorithm & State Layout
- Locking Semantics & API
- Comparison to Alternative Approaches
- When to use
- Quick Start
- Benchmarks & Scaling Performance
- Project Structure
- Building Test Code
- Conclusion
- License
In highly concurrent C++20 applications, reader-writer locks are useful for protecting data that is frequently read but less frequently mutated. However, the fairness characteristics of the underlying synchronization primitive can become important when contention is sustained.
Standard reader-writer primitives generally do not expose a portable policy for controlling how long a particular reader or writer may remain waiting. Their exact scheduling and wake-up behavior is implementation- and platform-dependent.
Under sustained contention, this can produce:
- Unpredictable Tail Latencies: A waiting thread may remain unscheduled or repeatedly lose acquisition opportunities due to scheduler behavior and contention.
- Reader/Writer Imbalance: A continuous stream of readers can delay waiting writers, while aggressive writer preference can conversely delay readers.
- Limited Fairness Control: Applications generally cannot configure explicit starvation thresholds, writer batching, or escalation behavior through the standard reader-writer-lock interface.
FairRWLock moves the fairness policy into user space while retaining an atomic fast path for uncontended acquisition.
The objective is not to replace the operating system scheduler. Instead, the lock controls lock admission and waiter selection so that prolonged contention changes the acquisition policy before starvation becomes unbounded by the lock's own arbitration logic.
FairRWLock uses a hybrid architecture consisting of an atomic fast path and a spinlock/semaphore slow path.
The uncontended read path avoids the internal queue lock and uses a single atomic increment.
On supported platforms where std::atomic<uint64_t> is lock-free, this operation is lock-free at the hardware atomic level. The overall lock is not lock-free because its contention path uses an atomic spinlock and C++20 semaphores.
Readers use a speculative atomic increment:
reader
|
v
atomic increment
|
+-- no WRITE_LOCKED / WRITER_STARVING --> acquire
|
+-- contested --------------------------> rollback + slow path
This allows the common uncontended reader case to avoid the internal queue lock entirely.
Writers use a compare-and-swap (CAS) operation on the packed state. If the lock is immediately available and the configured fairness limits permit acquisition, the writer can acquire without entering the slow path.
When the fast path cannot safely grant access, the operation enters the slow path. The slow path uses:
- a Test-and-Test-and-Set (TTAS)
std::atomic_flagspinlock for coordinating queue state; - a
std::counting_semaphorefor waiting readers; - an intrusive FIFO linked list for waiting writers;
- per-writer
std::binary_semaphorefor targeted writer notification.
Waiting writer nodes are owned by the waiting writer itself. This avoids dynamic allocation during lock acquisition. The writer queue therefore provides an explicit ordering mechanism rather than leaving writer selection entirely to OS-level wake-up behavior.
The lock explicitly identifies the next eligible waiter. For example, when the final active reader leaves while writers are queued, the lock can identify the writer at the head of the writer queue and notify that writer directly.
This is a logical baton handoff, not a scheduler-level transfer of execution. The notification identifies the preferred next owner, but the operating system still determines when that thread actually executes.
The distinction is important:
- FairRWLock controls: who should acquire next
- Operating system controls: when that thread actually runs
The implementation uses 64-byte alignment by default and 128-byte alignment on Apple ARM64. This is intended to reduce false sharing around frequently accessed atomic state. The exact benefit is microarchitecture- and workload-dependent, so benchmark results should be used to validate the choice for a particular deployment CPU.
The core feature of FairRWLock is its configurable fairness policy, represented by FairRWLockPolicy.
The policy parameters form a progression rather than being completely independent knobs. Under sustained contention, the lock progressively changes admission and priority behavior.
writer arrives
|
v
+--------------+
| Normal Mode |
+--------------+
|
starvationThreshold
|
v
+------------------+
| Writer Starving |
| reader gate set |
+------------------+
|
maxWriterWait
|
v
+------------------+
| Override Mode |
| writer priority |
+------------------+
/ \
limit reached timeslice active
/ \
v |
+----------------+ |
| Reader Phase | <-----+
+----------------+
The important distinction is between reader admission control and writer handoff.
When a writer has waited beyond starvationThreshold, the lock asserts WRITER_STARVING. This does not forcibly schedule the writer. Instead, it changes the fast-path admission rules so that new readers can no longer continue bypassing the queued writer.
If the lead writer continues waiting beyond maxWriterWait, the lock can enter Override Mode. Override Mode gives queued writers stronger priority while still applying configurable writer limits.
| Policy | Description | Default |
|---|---|---|
starvationThreshold |
Time a queued writer waits before WRITER_STARVING is asserted and new fast-path readers are gated. |
3 ms |
maxWriterWait |
Time after which prolonged writer waiting can trigger Override Mode. | 50 ms |
overrideTimeslice |
Maximum duration of the current Override Mode phase. | 100 ms |
writerBatchLimit |
Maximum number of writers that may acquire consecutively during an override phase before reader admission is restored. | 2 |
maxConsecWriters |
Optional maximum number of consecutive writers under normal operation. 0 disables the limit. | 0 |
maxYieldsBeforeBypass |
Number of scheduler-yield attempts made when a writer reaches a fairness limit before allowing the writer to bypass that limit when necessary. | 10 |
maxWriterWait is a fairness escalation threshold, not a hard real-time deadline. The lock cannot guarantee a maximum wall-clock acquisition time because thread scheduling remains under the operating system.
When a queued writer reaches starvationThreshold, the lock sets WRITER_STARVING in the atomic state.
The purpose is to stop a continuous stream of new readers from repeatedly taking the fast path while an existing writer is waiting.
The threshold is therefore an admission-control threshold, not a scheduling deadline.
Conceptually:
- Before threshold:
readers ---> fast path ---> acquire|writer ---> queue - After threshold:
readers ---> fast path ---> rejected|writer ---> queue/head ---> priority
Existing readers are allowed to finish normally. The objective is to prevent new readers from extending the active reader phase indefinitely.
If the lead writer remains queued beyond maxWriterWait, the lock can enter Override Mode.
Override Mode:
- marks the lock as writer-starving;
- records an override expiration time;
- resets the writer batch counter;
- gives queued writers priority;
- limits the number of consecutive writers through
writerBatchLimit; - eventually permits a reader phase once the configured writer limits are reached.
Override Mode is therefore an escalation mechanism for severe writer starvation, rather than the normal operating mode of the lock.
Writer preference is intentionally not allowed to continue indefinitely.
Once the relevant writer limit is reached, the lock can clear the starvation gate and wake waiting readers.
This produces a controlled cycle:
readers → writer contention → writer gate → writer acquisition → writer batch / override limit → reader phase → normal arbitration
The exact behavior is controlled by the configured policy rather than by a fixed reader-preference or writer-preference rule.
The lock separates the common uncontended case from contention management.
+----------------+
| Lock operation |
+-------+--------+
|
+------------+------------+
| |
Fast path Slow path
| |
atomic state spinlock + semaphores
| |
+------+-------+ +--------+--------+
| | | |
Reader Writer Readers Writers
| | | |
fetch-add CAS counting_sem FIFO queue
The fast path is deliberately small. The slow path contains the fairness machinery.
The primary lock state is stored in std::atomic<uint64_t> m_state.
The state is divided into an upper 32-bit writer streak and a lower 32-bit state field.
63 32 31 28 27 0
+----------------------------+-----------+-----------+
| Consecutive writer count | flags | readers |
+----------------------------+-----------+-----------+
The lower 32 bits contain:
bit 28:WRITER_STARVINGbit 30:WRITE_LOCKEDbit 31:HAS_WAITERSbits 0-27: active reader count
The upper 32 bits contain:
bits 32-63: consecutive writer count
The reader count therefore uses 28 bits of the packed state. The packed representation allows the fast path to inspect reader activity, writer ownership, and fairness state with a single atomic value.
The upper 32 bits track the number of consecutive writer acquisitions.
A successful writer acquisition increments the streak.
A reader acquisition clears the streak.
This allows the policy to distinguish between writer -> writer -> writer -> writer and writer -> readers -> writer. The distinction is important for maxConsecWriters and writer batching.
Waiting writers are represented by intrusive nodes:
struct WriterNode
{
WriterNode* prev;
WriterNode* next;
std::binary_semaphore sem{0};
time_point ts;
};The queue is maintained as m_headWriter to m_tailWriter. The queue is FIFO for writers entering the slow path.
Each waiting writer retains ownership of its own queue node for the duration of its wait. The node is removed when the writer acquires the lock or abandons the wait due to timeout.
This design avoids dynamic allocation during lock acquisition.
Each writer records its queue-entry timestamp.
The lead writer is evaluated against starvationThreshold and maxWriterWait.
The first threshold asserts WRITER_STARVING.
The second can activate Override Mode.
The use of std::chrono::steady_clock ensures that these durations are measured against a monotonic clock rather than wall-clock adjustments.
Readers first attempt the atomic fast path. A reader can acquire immediately when the relevant state permits it.
If WRITE_LOCKED or WRITER_STARVING prevents fast-path acquisition, the speculative reader increment is rolled back and the reader enters the slow path.
This is important because the starvation mechanism does not require every reader to acquire the internal spinlock. Only readers encountering the fairness gate or another contention condition need to enter the slow path.
Writers first inspect the packed state. If there are no active readers, no active writer, and no blocking fairness limit, the writer attempts a CAS to acquire the write bit. If the fast path cannot acquire the lock, the writer enters the FIFO queue. Only the writer at the head of the queue is eligible for normal queued acquisition. This is the primary mechanism by which the lock avoids repeatedly selecting arbitrary waiting writers.
When the lead writer encounters a configured writer limit while waiting for the appropriate reader phase, the implementation can temporarily yield to the operating-system scheduler.
The policy parameter maxYieldsBeforeBypass limits the number of these yield attempts.
The purpose is to avoid turning a fairness limit into an indefinite software wait when the expected reader transition does not occur.
After the configured number of yield attempts, the writer may bypass the limit when the acquisition conditions permit it.
This is a deliberate graceful-degradation mechanism rather than a spin loop.
FairRWLock provides both manual lock/unlock operations and RAII guards.
bool ReadLock(duration timeout = duration::max());
Attempts to acquire shared/read access.
Returns true if acquired, or false if the timeout expires before acquisition. The default duration::max() means wait indefinitely.
bool WriteLock(duration timeout = duration::max());
Attempts to acquire exclusive/write access.
Returns true if acquired, or false if the timeout expires before acquisition.
bool TryReadLock() noexcept;
Attempts to acquire read access without intentionally blocking.
The operation may inspect the slow-path spinlock using std::try_to_lock, but does not wait for the queue lock or for another lock holder.
bool TryWriteLock() noexcept;
Attempts to acquire write access without intentionally blocking.
As with TryReadLock(), the slow-path queue lock is only attempted with std::try_to_lock.
void ReadUnlock() noexcept;
Releases a read acquisition.
When the final active reader leaves and writers are waiting, the unlock operation can trigger the writer baton handoff.
void WriteUnlock() noexcept;
Releases a write acquisition.
The unlock path evaluates the active fairness policy and determines whether to hand off to the next writer, transition to a reader phase, clear an override, or return to normal arbitration.
void SetLogger(std::function<void(std::string_view)> lg);
Assigns an optional diagnostic logger for debugging lock state transitions.
This function is active in Debug builds (#ifndef NDEBUG) and compiles to a zero-cost abstraction in Release builds. To prevent data races and unsafe functor replacement during active lock contention, re-attachment is forbidden and will throw a std::logic_error.
The preferred usage model is RAII via FairRWLock::ReadGuard and FairRWLock::WriteGuard.
Both guards:
- acquire the lock during construction;
- release it during destruction if acquisition succeeded;
- are non-copyable;
- are movable;
- support optional acquisition timeouts;
- provide
operator bool()to test whether acquisition succeeded.
FairRWLock<> lock;
{
FairRWLock<>::ReadGuard guard(lock);
if (guard)
{
// Protected read access.
}
}A timeout can be supplied:
using namespace std::chrono_literals;
FairRWLock<>::WriteGuard guard(lock, 50ms);
if (guard)
{
// Protected write access.
}
else
{
// Acquisition timed out.
}FairRWLock is intended for conventional non-recursive reader/writer locking.
The lock does not provide a read-to-write upgrade operation or a write-to-read downgrade operation.
Applications should use the RAII guards where practical to ensure that successful acquisitions are released on every control-flow path.
Timeouts use std::chrono::steady_clock.
This means timeout measurement is monotonic and is not affected by changes to the system wall clock.
A timeout is a maximum waiting interval for acquisition; it is not a guarantee that the thread will be scheduled at any particular instant.
The project includes simpler reader-writer lock implementations to provide architectural points of comparison.
The standard C++ reader-writer primitive provides an established, portable interface.
Its primary advantages are a standard C++ API, mature implementations, minimal application-level complexity, and platform-specific optimization beneath the standard interface.
However, the standard interface does not expose a configurable fairness policy equivalent to FairRWLockPolicy. FairRWLock instead makes fairness escalation an explicit part of the algorithm.
| Property | std::shared_mutex |
FairRWLock |
|---|---|---|
| Standard C++ API | ✓ | — |
| Shared/exclusive locking | ✓ | ✓ |
| Configurable starvation threshold | — | ✓ |
| Explicit writer queue | Implementation-dependent | ✓ |
| Writer starvation escalation | Implementation-dependent | ✓ |
| Configurable writer batching | — | ✓ |
| Atomic fast-path state | Implementation-dependent | ✓ |
| Application-controlled fairness policy | — | ✓ |
| Contention Primitives | OS-dependent | Spinlock + Semaphores |
FairRWLock does not attempt to claim a hard real-time guarantee that std::shared_mutex lacks. Its distinction is that fairness policy is explicitly implemented and configurable.
TextbookReaderPrefLock represents a conventional textbook reader-preference implementation using one mutex, reader and writer condition variables, and explicit active-reader and active-writer state.
Its policy is intentionally simple: If no writer is active, readers may acquire. Waiting writers do not prevent new readers.
Under sustained read traffic, this can allow writers to remain waiting indefinitely. FairRWLock changes that behavior by allowing a waiting writer to eventually assert a reader admission gate.
The architectural trade-off is additional complexity in exchange for explicit starvation management and an atomic reader fast path.
MomentumRWLock uses a mutex/CV design that attempts to manually toggle priority between readers and writers.
It uses a priority flag to favor a waiting writer after the active reader/writer phase completes.
The primary architectural difference is that MomentumRWLock performs its state coordination through an OS-level mutex, whereas FairRWLock separates the common uncontended path (atomic state) from contention/fairness management (atomic spinlock + C++20 semaphores + writer queue).
FairRWLock therefore introduces substantially more state and policy machinery in exchange for avoiding the internal mutex on the common atomic fast path.
FairRWLock occupies a specific architectural niche. It is optimized for systems that require the high throughput of an optimistic lock-free fast path, but cannot afford the unpredictable tail latencies of OS-mediated lock contention.
- Predictable Writer Latency (Soft Real-Time): Ideal for ETW/EDR telemetry pipelines, financial trading routing tables, or configuration management where an endless stream of parallel reads must not permanently starve a critical write/update operation.
- Cross-Platform Determinism: When you require identical lock-arbitration and admission semantics across Windows, Linux, and macOS. It normalizes behavior so you are not subjected to the whims of the OS scheduler (such as 100% POSIX writer starvation on Linux, or 50+ ms SRW latency spikes on Windows).
- Zero-Allocation Contention Paths: When your hot paths strictly forbid heap allocations. The intrusive queue allocates
WriterNodestructures directly on the waiting thread's stack. - Architecture-Specific Tuning: When deploying to platforms with known false-sharing footprints, allowing you to leverage the explicit 128-byte cache-line padding (e.g., Apple Silicon / ARM64).
- Massive NUMA / High-Core-Count Read Scaling: If you are deploying to 64+ core server topologies where read traffic is continuous and highly parallel, the single atomic
m_statecounter will become a bottleneck due to global L1 cache-line invalidation (ping-ponging). For extreme read-scaling on multi-socket machines, consider cache-striped deferred locks likefolly::SharedMutex. - Hard Real-Time Systems: If your architecture requires strict Worst-Case Execution Time (WCET) bounds, this lock's graceful degradation and yield backoffs are inappropriate. Use a strict Phase-Fair lock (
pflock) instead. - Low-Contention General Purpose Logic: If thread starvation is not a measured issue in your application profile, the standard
std::shared_mutexprovides a simpler dependency and perfectly adequate performance.
Include the header:
#include "fair_rw_lock.h"
#include <iostream>
FairRWLock<> rwLock;
void ReadData()
{
FairRWLock<>::ReadGuard guard(rwLock);
if (guard)
{
std::cout << "Reading data securely...\n";
}
}
void WriteData()
{
FairRWLock<>::WriteGuard guard(rwLock);
if (guard)
{
std::cout << "Writing data securely...\n";
}
}The policy can be configured when constructing the lock:
using namespace std::chrono_literals;
// Derive from the default policy to inherit base types and unchanged limits
struct PersistencePolicy : DefaultFairRWLockPolicy
{
static constexpr auto maxWriterWait = 15ms;
static constexpr auto overrideTimeslice = 50ms;
static constexpr int writerBatchLimit = 2;
static constexpr int maxConsecWriters = 3;
static constexpr auto starvationThreshold = 4ms;
};
FairRWLock<PersistencePolicy> rwLock;The policy should be tuned according to the workload rather than treated as a universal optimal configuration.
Manual locking is also supported:
if (rwLock.ReadLock(10ms))
{
// Protected read access.
rwLock.ReadUnlock();
}For writes:
if (rwLock.WriteLock(50ms))
{
// Protected write access.
rwLock.WriteUnlock();
}To evaluate FairRWLock, the included benchmark suite compares it against the standard C++ reader-writer implementation (std::shared_mutex), a textbook reader-preference implementation (TextbookReaderPrefLock), and a momentum-based implementation (MomentumRWLock) across different hardware and operating-system environments.
The complete benchmark and test suite is included in the repository for reproducibility.
- Target Platforms: Windows 10/11.
- Compiler: The test suite was compiled with MSVC 2026.
- Workload: High-frequency mixed-contention scenarios scaled to the logical core count of each CPU, with two writers and the remaining logical cores used as readers.
- Starvation Testing: Continuous reader oversubscription is used to intentionally stress writer acquisition, including workloads with up to 40 concurrent readers.
- Benchmark Duration: Throughput tests run continuously for 15 seconds per implementation.
The following results show total operations processed per second during the sustained mixed-contention benchmark:
| Hardware Topology | std::shared_mutex Ops/Sec |
FairRWLock Ops/Sec |
Total Ops Speedup |
|---|---|---|---|
| Intel Core i7-8086K (Desktop, 12-Thread) | ~8.38 Million | ~11.95 Million | +42.48% Faster |
| Intel Core i7-1165G7 (Mobile, 8-Thread) | ~7.92 Million | ~18.81 Million | +137.48% Faster |
| Intel Core i7-12700H (Hybrid, 20-Thread) | ~7.85 Million | ~20.53 Million | +161.33% Faster |
The results show a substantial throughput advantage for FairRWLock on the tested Windows systems. The magnitude of the improvement varies with CPU architecture and workload.
This test evaluates how long a writer must wait to acquire the lock while subjected to a continuous stream of read requests. For this benchmark, the starvationThreshold in the FairRWLock policy was explicitly set to 3 ms.
| Implementation | i7-8086K (24 Readers) | i7-1165G7 (16 Readers) | i7-12700H (40 Readers) |
|---|---|---|---|
| TextbookReaderPrefLock | 100% Starvation | 100% Starvation | 100% Starvation |
| MomentumRWLock | 100% Starvation | 100% Starvation | 100% Starvation |
std::shared_mutex |
0.50 ms avg wait | 0.48 ms avg wait | 0.49 ms avg wait |
| FairRWLock | 2.88 ms avg wait | 3.24 ms avg wait | 2.93 ms avg wait |
On Windows, the tested std::shared_mutex implementation is backed by Slim Reader/Writer (SRW) locks, which perform well under reader oversubscription and successfully prevent the total writer starvation observed in simpler implementations.
However, since scheduling remains dependent on the operating system, the tested std::shared_mutex implementation exhibited significant writer latency spikes under peak contention. In our benchmark latency distributions, maximum observed writer latencies ranged from 39.38 ms to 57.22 ms across the tested hardware topologies.
FairRWLock provides application-controlled fairness rather than relying entirely on platform-specific scheduling behavior. With a 3 ms starvationThreshold, average writer wait times remained close to the configured threshold (~2.88 ms to 3.24 ms), while maximum observed writer latencies were 5.01 ms to 17.67 ms, depending on policy configuration and thread count.
This threshold functions as an admission-control gate rather than a hard real-time scheduling deadline; the operating system still determines when a waiting thread actually executes.
The throughput results represent the primary difference on the tested Windows systems, with FairRWLock ranging from 42.48% to 161.33% higher throughput than std::shared_mutex under mixed contention.
- Target Platforms: Fedora and Ubuntu running as VMware guests with 8 logical CPUs on an Intel Core i7-8086K host system.
- Compilers: The Fedora test suite was compiled with g++ 16, and the Ubuntu test suite used g++ 15.
- Workload: High-frequency mixed-contention scenarios using 6 readers and 2 writers.
- Starvation Testing: Continuous reader oversubscription using 16 concurrent readers to intentionally stress writer acquisition.
- Benchmark Duration: Throughput tests run continuously for 15 seconds per implementation.
The following results show total operations processed per second during the sustained mixed-contention benchmark:
| Linux Distribution | std::shared_mutex Ops/Sec |
FairRWLock Ops/Sec |
Total Ops Speedup |
|---|---|---|---|
| Fedora (VM, 8-Core) | ~6.41 Million | ~6.75 Million | +5.27% Faster |
| Ubuntu (VM, 8-Core) | ~6.04 Million | ~6.34 Million | +5.06% Faster |
The throughput advantage on the tested Linux systems is modest (+5.06% to +5.27%) compared to Windows, demonstrating that relative lock performance is highly dependent on platform and workload.
This test evaluates writer acquisition under sustained reader pressure. The starvationThreshold was again set to 3 ms.
| Implementation | Fedora (16 Readers) | Ubuntu (16 Readers) |
|---|---|---|
| TextbookReaderPrefLock | 100% Starvation | 100% Starvation |
| MomentumRWLock | 100% Starvation | 100% Starvation |
std::shared_mutex |
100% Starvation | 100% Starvation |
| FairRWLock | 3.18 ms avg wait | 3.25 ms avg wait |
On Linux, std::shared_mutex is typically implemented using POSIX pthread_rwlock_t, which can exhibit strong reader-preference characteristics under sustained reader oversubscription.
During the starvation benchmark, waiting writers were unable to acquire std::shared_mutex, reaching the 15.00-second test timeout. In contrast, FairRWLock transferred lock admission to waiting writers at ~3.18 ms to 3.25 ms in accordance with its 3 ms policy, with maximum observed writer latencies of 4.27 ms to 4.96 ms.
This difference represents the primary result on Linux: FairRWLock provides a consistent application-level fairness policy across platforms, preventing application-level writer starvation from depending entirely on the fairness behavior of the platform's standard-library implementation.
As with Windows, the configured starvation threshold should be understood as an admission-control policy rather than a hard real-time execution deadline.
The benchmark results highlight several key observations:
- Windows Throughput:
FairRWLockdemonstrated substantial throughput gains overstd::shared_mutex, ranging from +42.48% to +161.33% on the tested systems. - Linux Fairness: While Linux throughput gains were modest (~+5%),
FairRWLockprevented the 100% writer starvation observed instd::shared_mutexunder the tested workload, reducing observed writer wait times from the 15.00-second test timeout to under 5 ms. - Tail Latency: On Windows, the tested
std::shared_muteximplementation exhibited maximum writer latency spikes of 39 ms – 57 ms, whileFairRWLockrecorded maximum observed writer latencies of ~5 ms – 17 ms depending on workload and policy configuration. - Platform Differences:
std::shared_mutexbehavior varied significantly between Windows (SRW lock-backed) and Linux (POSIX-backed), highlighting that standard primitives do not guarantee identical scheduling or fairness behavior across platforms. - Fairness Policy: The simpler implementations consistently suffered from starvation under continuous reader traffic, whereas
FairRWLockpredictably granted admission around the configured policy thresholds.
These results reflect empirical observations under specific test configurations rather than universal performance guarantees. Different CPU microarchitectures, operating systems, standard library implementations, thread counts, and read/write ratios will yield different results.
The complete benchmark harness is included with the project so that these measurements can be reproduced and evaluated under custom application workloads.
The repository is organized into distinct layers to separate the core lock logic from the cross-platform test suite:
-
fair_rw_lock/: Contains the fair RW lock implementation.fair_rw_lock.h: The cross-platform C++20 header for Linux, macOS, and Windows applications.
-
test/: Cross-platform user-mode performance test suite.fair_rw_lock_test.cpp: Test suite implementation.textbook_reader_pref_lock.h: Provided for reference; a classic implementation of a reader-preference lock.momentum_rw_lock.h: Provided for reference; a momentum RW lock (manages lock handover to pass control between reading and writing phases).
Requires a C++20 compliant compiler (GCC or Clang).
Using the Build Script (Recommended)
./build.sh [--clean | -c] [--type Release | Debug] [--compiler g++ | clang++]
Defaults:
- Build type: Release
- Compiler: g++
- Reuses existing build/
Options:
-c, --clean → Remove build/ before building
-t, --type → Set build type (Release or Debug)
--compiler → Choose compiler (g++ or clang++)
-h, --help → Show help and exit
#Set permissions (once):
chmod +x ./build.sh
# Standard Release build using default C++ compiler
./build.sh
# Clean Release build using clang++ (Optimized for macOS or specific Linux tests)
./build.sh --clean --type Release --compiler clang++
# Debug build for troubleshooting
./build.sh --clean --type DebugUsing CMake
The included CMakeLists.txt automatically detects your platform, handles threading library links (Threads::Threads), and configures optimized build flags (including IPO/LTO).
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j $(nproc 2>/dev/null || sysctl -n hw.ncpu)Native Visual Studio 2026 Solution (.slnx) and Project (.vcxproj) files are included in the repository.
- Visual Studio 2026: Requires C++20 support (
v145toolset).
By moving fairness policies into user space and separating the atomic uncontended path from the managed queue, FairRWLock resolves the unpredictable tail latencies inherent in OS-mediated reader-writer primitives. The core advantage is predictable admission control: rather than relying on the black-box scheduling of POSIX or SRW locks, the application deterministically caps writer starvation at the lock-arbitration level.
Based on this architecture's mechanical sympathy, the performance and fairness advantages will compound across specific emerging hardware topologies and deployment environments:
- High-Core-Count Microarchitectures (Threadripper / Xeon / EPYC): On systems with massive core counts, continuous read-streams can permanently starve writers using standard primitives due to localized cache coherence storms. The logical baton handoff mitigates this by preemptively gating new fast-path readers, ensuring configuration updates or writer phases execute predictably regardless of the parallel read volume.
- Asymmetric / Hybrid Architectures (Intel Big.LITTLE): Thread scheduling volatility across P-Cores and E-Cores can cause waiting writers to yield inconsistently. The tiered hardware backoff and bounded scheduler-yield limits (
maxYieldsBeforeBypass) specifically prevent threads descheduled on slower E-Cores from deadlocking the fairness state machine, safely bypassing limits if reader transitions stall. - ARM64 Cloud Instances (AWS Graviton / Apple Silicon): The deliberate 128-byte cache-line alignment specifically targets the destructive interference patterns common on Apple Silicon and modern ARM server chips. Combined with optimized
compare_exchange_weakusage on the fast path, the lock actively avoids nested hardware spin-loops on processors with weak memory models. - Cross-Platform Telemetry & EDR Pipelines: Since standard primitive behavior diverges wildly between Windows (SRW locks) and Linux (POSIX rwlocks), high-throughput cross-platform pipelines often suffer from inconsistent telemetry drops under peak load. This user-space fairness state machine guarantees an identical lock admission profile across OS environments, bounding worst-case writer latency to the configured starvation thresholds rather than OS scheduler quirks.
This project is licensed under the Apache License, Version 2.0.
You may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the LICENSE file for the specific language governing permissions and limitations.