swmr-cell 0.3.0

A thread-safe single-writer multi-reader cell with wait-free reads and version-based garbage collection
Documentation
swmr-cell-0.3.0 has been yanked.

SWMR-Cell: Version-Based Single-Writer Multi-Reader Cell

Crates.io Documentation License

中文文档

swmr-cell provides a thread-safe SwmrCell that supports concurrent wait-free reads and lock-free writes (amortized) using version-based garbage collection. It is designed for high-performance scenarios where a single writer updates data frequently while multiple readers access it concurrently.

Features

  • Single-Writer Multi-Reader (SWMR): Optimized for one writer thread and multiple concurrent reader threads.
  • Wait-Free Reads: Readers obtain data snapshots without locking, blocking, or interfering with the writer.
  • Version-Based Garbage Collection: Old data is automatically reclaimed when no readers are observing it, using a mechanism similar to RCU (Read-Copy-Update) or Epoch-based GC.
  • Snapshot Semantics: A reader holds a consistent view of the data as long as the PinGuard is alive.
  • Minimal Synchronization:
    • Readers use atomic operations to register their active version.
    • The writer uses a mutex only for managing reader registration and scanning reader states during collection.
  • Configurable GC: Supports automatic or manual garbage collection with tunable thresholds.
  • no_std Support: Compatible with no_std environments using alloc and spin.
  • Type-Safe: Full Rust ownership and lifetime safety.

Usage

Add swmr-cell to your Cargo.toml:

[dependencies]
swmr-cell = "0.3"

Read-Preferred Mode

By default, swmr-cell is optimized for write-heavy or balanced workloads. You can enable read-preferred mode via the builder, which uses specialized heavy/light memory barriers (via swmr-barrier). This makes readers wait-free in instruction cycles by shifting synchronization cost to the writer.

use swmr_cell::SwmrCell;

let mut cell = SwmrCell::builder()
    .read_preferred() // Optimize for read performance
    .build(42);

no_std Support

To use swmr-cell in a no_std environment, disable the default features and enable the spin feature. Note that this crate relies on alloc, so a global allocator must be available.

[dependencies]
swmr-cell = { version = "0.3", default-features = false, features = ["spin"] }

Basic Example

use swmr_cell::SwmrCell;

// 1. Create a new SWMR cell with an initial value
let mut cell = SwmrCell::new(42i32);

// 2. Create a local reader for this thread (each thread needs its own local reader)
let local = cell.local_reader();

// 3. Pin and read the value
// The guard provides a snapshot of the value at the moment of pinning
let guard = local.pin();
assert_eq!(*guard, 42);
drop(guard); // Reader is no longer accessing the data

// 4. Writer updates the value
// This creates a new version; old version is retired
cell.store(100i32);

// 5. Read the new value
let guard = local.pin();
assert_eq!(*guard, 100);

Threaded Example

use swmr_cell::SwmrCell;
use std::thread;

fn main() {
    let mut cell = SwmrCell::new(0);
    
    let mut reader_handles = vec![];
    
    for i in 0..3 {
        // Create a LocalReader for each thread
        let local = cell.local_reader();
        
        let handle = thread::spawn(move || {
            loop {
                let guard = local.pin();
                let val = *guard;
                println!("Reader {} saw: {}", i, val);
                if val == 10 { break; }
                drop(guard); // unpin before sleeping or doing other work
                thread::sleep(std::time::Duration::from_millis(10));
            }
        });
        reader_handles.push(handle);
    }
    
    // Writer updates values
    for i in 1..=10 {
        cell.store(i);
        thread::sleep(std::time::Duration::from_millis(20));
    }
    
    for h in reader_handles {
        h.join().unwrap();
    }
}

Using SwmrReaderFactory for Shared Reader Creation

If you need to distribute the ability to create readers to multiple threads (e.g., in a thread pool where threads are dynamic), you can use SwmrReaderFactory. Unlike LocalReader, SwmrReaderFactory is Sync and Clone.

use swmr_cell::SwmrCell;
use std::thread;

let mut cell = SwmrCell::new(0);

// Create a SwmrReaderFactory that can be shared
let reader_factory = cell.reader_factory();

for i in 0..3 {
    // Clone the factory for each thread
    let factory = reader_factory.clone();
    
    thread::spawn(move || {
        // Create a LocalReader on the thread using the factory
        let local = factory.local_reader();
        
        // ... use local reader ...
    });
}

You can also obtain a SwmrReaderFactory from an existing LocalReader if you need to pass the capability to another thread:

// 1. Share: Create a SwmrReaderFactory from a LocalReader
let local_reader = cell.local_reader();
let factory = local_reader.reader_factory(); 
thread::spawn(move || {
    let local = factory.local_reader();
    // ...
});

// 2. Convert: Consume LocalReader to get SwmrReaderFactory
let local_reader = cell.local_reader();
let factory = local_reader.into_swmr(); // Consumes local_reader
thread::spawn(move || {
    let local = factory.local_reader();
    // ...
});

Writer Utilities

SwmrCell provides several utilities for the writer:

let mut cell = SwmrCell::new(10);

// Update value using a closure
cell.update(|v| v + 5);
assert_eq!(*cell.get(), 15);

// Access the previous (retired) value
// Useful for rollback or comparison
assert_eq!(cell.previous(), Some(&10));

Configuration

You can customize the garbage collection behavior using SwmrCell::builder():

use swmr_cell::SwmrCell;

let mut cell = SwmrCell::builder()
    .auto_reclaim_threshold(Some(128)) // Trigger GC automatically after 128 stores (default: 16)
    // .auto_reclaim_threshold(None)   // Disable automatic GC
    .build(0);
  • auto_reclaim_threshold: Controls how many retired objects are buffered before scanning readers for reclamation. Larger values reduce GC overhead but increase memory usage.

How It Works

  1. Versions: The cell maintains a global monotonic version counter. Each store increments the version.
  2. Readers: Each LocalReader has a slot where it publishes the version it is currently accessing (active_version).
  3. Reclamation: When the writer updates the value, the old value is added to a garbage queue with the version it belonged to.
  4. Collection: The writer scans all active reader slots. It calculates the minimum version currently visible to any reader. Any garbage from versions strictly older than this minimum is safe to reclaim.

Safety

  • LocalReader is !Sync and must remain on the thread that uses it.
  • PinGuard is bound to the lifetime of the LocalReader and prevents the underlying data from being freed while held.
  • Accessing the data via PinGuard is safe because the writer guarantees that no version visible to an active reader will be deallocated.

License

This project is licensed under either of

at your option.