Skip to content

Capmare/LocklessLib

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LocklessLib

A high-performance, lock-free ring buffer library for C++23. This library provides three thread-safe queue implementations designed for low-latency inter-thread communication without mutex locks.

Overview

LocklessLib implements efficient ring buffers for various multi-threaded scenarios. Each implementation uses atomic operations and careful memory ordering to provide thread-safe operations without blocking locks. This approach reduces latency and improves throughput in concurrent systems.

Ring Buffer Implementations

SPSC (Single Producer Single Consumer)

The simplest implementation designed for exactly one producer and one consumer thread. Uses relaxed atomic operations since there is no contention between producers or consumers.

  • File: include/lockless/SPSCRingBuffer.h
  • Thread Safety: 1 producer, 1 consumer
  • Lock Type: Lock-free using atomics
  • Use Case: Dedicated producer-consumer pipelines

MPSC (Multiple Producers Single Consumer)

Designed for multiple producers writing to a single consumer. The consumer has exclusive read access, allowing relaxed operations on the consumer side. Producers must coordinate using compare-and-swap operations on the tail pointer.

  • File: include/lockless/MPSCRingBuffer.h
  • Thread Safety: N producers, 1 consumer
  • Lock Type: Lock-free using atomic CAS operations
  • Use Case: Fan-in scenarios where multiple threads send data to one reader
  • Implementation Details: Uses per-slot Ready flags to coordinate multiple producers

MPMC (Multiple Producers Multiple Consumers)

The most general implementation supporting multiple producers and multiple consumers simultaneously. Uses sequence numbers on each slot to coordinate access from both sides.

  • File: include/lockless/MPMCRingBuffer.h
  • Thread Safety: N producers, M consumers
  • Lock Type: Lock-free using atomic CAS and sequence numbers
  • Use Case: General work queues and fan-out scenarios
  • Implementation Details: Each slot contains a sequence number tracked with acquire/release semantics

Key Features

  • Lock-free: No mutexes or blocking operations
  • Wait-free for many operations: Producers and consumers operate independently
  • Cache-aware: Aligns head/tail pointers to cache line boundaries to prevent false sharing
  • Type-safe: Template-based generic implementation
  • Move semantics: Efficient transfer of non-copyable objects
  • Optional return values: Cleanly handles empty queue conditions
  • Memory ordering: Proper use of acquire/release semantics for correct synchronization

Building

Requirements

  • C++23 compiler (GCC 14.3+ or equivalent)
  • CMake 3.25 or later
  • pthreads library

Build Instructions

mkdir cmake-build-release
cd cmake-build-release
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)

This generates three benchmark executables:

  • spsc_bench: SPSC ring buffer benchmark
  • mpsc_bench: MPSC ring buffer benchmark
  • mpmc_bench: MPMC ring buffer benchmark

Benchmark Results

All benchmarks were run on an Intel Core i7-14700K processor (20 cores, 28 threads) with 3.4 GHz base frequency. The benchmarks measure latency in nanoseconds and throughput in nanoseconds per operation. Each benchmark compares lock-free implementations against a mutex-protected baseline.

SPSC (Single Producer Single Consumer) Benchmark

Lock-Free Implementation

Latency (2 million samples):

  • Average: 130.7 ns
  • p50: 135 ns
  • p90: 145 ns
  • p99: 152 ns
  • p99.9: 201 ns
  • p99.99: 3035 ns
  • max: 109156 ns

Throughput: 2 ns per operation

Mutex Implementation

Latency (2 million samples):

  • Average: 437.4 ns
  • p50: 405 ns
  • p90: 612 ns
  • p99: 1718 ns
  • p99.9: 2639 ns
  • p99.99: 4567 ns
  • max: 60631 ns

Throughput: 88 ns per operation

Speedup: Lock-free is 3.3x faster on average, 2 ns vs 88 ns per operation

MPSC (Multiple Producers Single Consumer) Benchmark

1 Producer

Lock-Free Latency:

  • Average: 4352.7 ns
  • p50: 135 ns
  • p99: 107429 ns
  • max: 289494 ns

Lock-Free Throughput: 73 ns per operation

Mutex Latency:

  • Average: 2050149.0 ns
  • p50: 2321914 ns
  • p99: 4704405 ns
  • max: 5019833 ns

Mutex Throughput: 87 ns per operation

2 Producers

Lock-Free Latency:

  • Average: 373.4 ns
  • p50: 238 ns
  • p99: 1250 ns
  • max: 23782 ns

Lock-Free Throughput: 85 ns per operation

Mutex Latency:

  • Average: 2660897.9 ns
  • p50: 2395798 ns
  • p99: 5568603 ns
  • max: 6644378 ns

Mutex Throughput: 121 ns per operation

4 Producers

Lock-Free Latency:

  • Average: 17590.7 ns
  • p50: 600 ns
  • p99: 771463 ns
  • max: 1152013 ns

Lock-Free Throughput: 88 ns per operation

Mutex Latency:

  • Average: 7624444.4 ns
  • p50: 7661451 ns
  • p99: 11172914 ns
  • max: 12842512 ns

Mutex Throughput: 342 ns per operation

8 Producers

Lock-Free Latency:

  • Average: 71690.9 ns
  • p50: 5527 ns
  • p99: 848163 ns
  • max: 1130582 ns

Lock-Free Throughput: 107 ns per operation

Mutex Latency:

  • Average: 15686195.1 ns
  • p50: 15975806 ns
  • p99: 19759307 ns
  • max: 21232077 ns

Mutex Throughput: 512 ns per operation

MPSC Summary: Lock-free remains consistently faster. With 8 producers, lock-free achieves 107 ns per operation vs mutex at 512 ns (4.7x faster).

MPMC (Multiple Producers Multiple Consumers) Benchmark

1 Producer x 1 Consumer

Lock-Free Latency (2 million samples):

  • Average: 1272828.7 ns
  • p50: 1417610 ns
  • p99: 1613264 ns
  • p99.9: 1643818 ns
  • p99.99: 1648730 ns
  • max: 1650061 ns

Lock-Free Throughput: 22 ns per operation

Mutex Latency:

  • Average: 2400992.6 ns
  • p50: 2448980 ns
  • p99: 2574226 ns
  • max: 2769348 ns

Mutex Throughput: 96 ns per operation

2 Producers x 2 Consumers

Lock-Free Latency:

  • Average: 1231068.6 ns
  • p50: 1409179 ns
  • p99: 1629619 ns
  • max: 1673148 ns

Lock-Free Throughput: 92 ns per operation

Mutex Latency:

  • Average: 2549241.4 ns
  • p50: 2918663 ns
  • p99: 3431708 ns
  • max: 3942785 ns

Mutex Throughput: 156 ns per operation

4 Producers x 2 Consumers

Lock-Free Latency:

  • Average: 257979.3 ns
  • p50: 892 ns
  • p99: 1471769 ns
  • max: 1539515 ns

Lock-Free Throughput: 92 ns per operation

Mutex Latency:

  • Average: 5531228.1 ns
  • p50: 5535228 ns
  • p99: 6837559 ns
  • max: 6970470 ns

Mutex Throughput: 225 ns per operation

4 Producers x 4 Consumers

Lock-Free Latency:

  • Average: 605375.6 ns
  • p50: 2835 ns
  • p99: 1734935 ns
  • max: 1766321 ns

Lock-Free Throughput: 112 ns per operation

Mutex Latency:

  • Average: 118025.0 ns
  • p50: 12643 ns
  • p99: 946373 ns
  • max: 1064597 ns

Mutex Throughput: 180 ns per operation

8 Producers x 4 Consumers

Lock-Free Latency:

  • Average: 222842.4 ns
  • p50: 1327 ns
  • p99: 1672039 ns
  • max: 1696826 ns

Lock-Free Throughput: 114 ns per operation

Mutex Latency:

  • Average: 6656876.3 ns
  • p50: 6777768 ns
  • p99: 7556705 ns
  • max: 8210550 ns

Mutex Throughput: 275 ns per operation

Performance Observations

  1. Consistent Lock-Free Advantage: Across all three queue types, lock-free implementations provide better or comparable latency. In SPSC and MPSC, the advantage is dramatic, with mutex latencies 5-200x higher at high percentiles.

  2. Throughput Gains: Lock-free achieves 2-5x better throughput compared to mutex-based implementations across most configurations. The SPSC shows the most dramatic improvement at 44x better throughput.

  3. Tail Latency Reduction: The most significant advantage appears in tail latencies (p99, p99.9, p99.99). Mutex implementations exhibit severe tail latencies due to lock contention, while lock-free implementations show more predictable behavior.

  4. Scalability: Lock-free implementations scale better with increasing thread counts. Mutex contention grows exponentially with more producers, while lock-free performance remains more stable.

  5. Trade-offs by Configuration: MPMC shows lower absolute latencies in some cases due to reduced per-message overhead with multiple consumers, but lock-free still maintains better peak performance and scalability.

Usage Example

#include <lockless/MPMCRingBuffer.h>
#include <thread>

int main() {
    // Create a lock-free queue with capacity for 256 items
    Lockless::MPMCRingBuffer<int, 256> queue;

    // Producer thread
    auto producer = std::thread([&]() {
        for (int i = 0; i < 100; ++i) {
            while (!queue.try_push(i)) {
                // Queue full, retry
            }
        }
    });

    // Consumer thread
    auto consumer = std::thread([&]() {
        for (int i = 0; i < 100; ++i) {
            while (true) {
                auto item = queue.try_pop();
                if (item) {
                    // Process item
                    break;
                }
            }
        }
    });

    producer.join();
    consumer.join();

    return 0;
}

API Reference

All three implementations provide the same interface:

// Push an item (lvalue reference)
[[nodiscard]] bool try_push(const T& value);

// Push an item (rvalue reference, for move semantics)
[[nodiscard]] bool try_push(T&& value);

// Pop an item if available
[[nodiscard]] std::optional<T> try_pop();

// Query capacity
[[nodiscard]] constexpr std::size_t capacity() const noexcept;

Returns:

  • try_push: Returns true if the item was enqueued, false if the queue is full
  • try_pop: Returns std::optional containing the item if available, or std::nullopt if empty

Implementation Details

Memory Ordering

All implementations use careful memory ordering to ensure correctness:

  • Acquire semantics for reads that must synchronize with other threads
  • Release semantics for writes that synchronize with other threads
  • Relaxed operations where there is no synchronization requirement

Cache Line Alignment

Head and tail pointers are aligned to cache line boundaries using alignas(cache_line_size). This prevents false sharing where modifications to head cause invalidation of the tail pointer's cache line.

Capacity Constraints

All ring buffer capacities must be powers of 2. This allows efficient index masking instead of modulo operations:

  • index = (index + 1) % Capacity becomes index = (index + 1) & (Capacity - 1)

Thread Safety

All implementations are fully thread-safe for the specified thread model. However:

  • Do not share a queue between threads in different producer or consumer groups than designed
  • Do not call operations from threads that are not designated producers or consumers
  • The queue itself is not thread-safe for resizing or destruction while in use

Known Limitations

  1. Fixed capacity: Ring buffers cannot grow dynamically. Size is determined at compile time.
  2. Try-only semantics: Operations never block. If the queue is full, try_push returns false. If empty, try_pop returns nullopt.
  3. No blocking waits: For scenarios requiring blocking behavior, use condition variables with these queues.
  4. Move-only: Some operations may move objects even on failure. Always check return values.

Testing

Each ring buffer includes a comprehensive sanity check in its benchmark that verifies:

  • Basic push/pop operations
  • Full queue conditions
  • Empty queue conditions
  • Single-threaded interleaved operations
  • Multi-threaded correctness with proper item counts and no duplicates

Run any benchmark executable to verify correctness and measure performance.

Design References

These implementations follow patterns from academic literature on lock-free data structures:

  • Sequence-based concurrency control for multiple producers and consumers
  • Atomic compare-and-swap for coordinating multiple threads
  • Acquire/release memory ordering for efficient synchronization
  • Cache-aware layout to minimize false sharing

License

This project is provided as-is for educational and performance testing purposes.

Author

Created by capmare on March 22, 2026.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors