arc-swap

repository·master·Indexed 23 days ago

https://github.com/vorner/arc-swap

A Rust library providing an optimized synchronization primitive for managing shared, atomically swappable Arc<T> pointers. Designed for high-performance, read-mostly, write-seldom workloads, it offers an alternative to RwLock<Arc<T>> by reducing CPU-level contention. Key features include the ArcSwap and ArcSwapOption types, Read-Copy-Update (RCU) operations via the rcu() method, atomic compare-and-swap, and data projections using map().

Tokens
3K
Snippets
6
Records
14
Agent score
79%

What's inside arc-swap

  1. What is ArcSwap?

    master
    ArcSwap provides a synchronization primitive similar to RwLock<Arc<T>> or a hypothetical Atomic<Arc<T>>. It is specifically optimized for read-mostly, write-seldom scenarios, offering consistent performance characteristics for workloads where reads are frequent and updates to the shared data are rare.
  2. What is ArcSwap and when to use it

    master

    Concept

    ArcSwap is a container for an Arc (or Option<Arc>) that allows atomic updates. It is designed for read-mostly scenarios where data is frequently read by many threads but updated infrequently (e.g., configuration settings, routing tables, or data snapshots).

    Why use it instead of RwLock?

    • RwLock<T>: Holding a read-lock for long periods can block writers, pausing all processing.
    • RwLock<Arc<T>>: While you can lock, clone the Arc, and unlock quickly, this causes CPU-level contention on the lock and the Arc reference count, making it slower in both contended and non-contended scenarios.
    • ArcSwap: Optimized for high-concurrency reads. It provides consistent performance and allows updates to occur without disrupting readers. Readers can continue using a consistent version of the data even while an update is in progress.
  3. The `Guard` type for temporary borrows

    master

    Concept

    When you call load(), ArcSwap returns a Guard<T, S>. This is a smart pointer that provides a temporary, cheap borrow of the object currently held inside the container.

    Key Characteristics

    • Efficiency: Guard is designed to be a lightweight proxy. It is best suited for local variables on the stack.
    • Consistency: To ensure logical consistency (e.g., reading multiple related fields from a struct), you should call load() once and keep the resulting Guard rather than calling load() multiple times. Calling load() multiple times might return different versions of the data between calls.
    • Conversion: You can convert a Guard back into the owned value using Guard::into_inner(lease).
  4. Use the HybridProtection strategy for memory reclamation

    master

    The HybridStrategy implements a hybrid protection mechanism for managing memory reclamation in arc-swap. It uses a two-tier approach for 'debts' (tracking references that an Arc may owe):

    1. Fast Path: Attempts to use a fast, fallible debt slot. This is efficient but can fail if slots are full or if there is concurrent writer interference.
    2. Fallback Path: A slower but guaranteed mechanism that uses a 'helping' slot to ensure the load succeeds.

    When using this strategy, the HybridProtection guard manages the lifecycle of the loaded pointer. When the guard is dropped, it automatically handles paying back debts or decrementing reference counts to ensure memory safety.

  5. Perform Read-Copy-Update (RCU) with `rcu`

    master

    Concept

    The rcu method is used for thread-safe updates that depend on the current value. It is essential when multiple threads might attempt to update the data simultaneously. If an update occurs while your closure is running, rcu will retry the closure with the new value.

    Best Practice: Minimize work inside rcu

    Because rcu may call your closure multiple times due to retries, you should perform expensive computations outside the rcu block. Only perform the cheap operations (like cloning the container and inserting the result) inside the closure.

    Example

    use std::collections::HashMap;
    use std::sync::Arc;
    use arc_swap::ArcSwap;
    use once_cell::sync::Lazy;
    
    type Cache = HashMap<usize, usize>;
    static CACHE: Lazy<ArcSwap<Cache>> = Lazy::new(|| ArcSwap::default());
    
    fn cached_computation(x: usize) -> usize {
        let cache = CACHE.load();
        if let Some(result) = cache.get(&x) { return *result; }
    
        // Expensive work done OUTSIDE rcu
        let result = x * 2; 
    
        CACHE.rcu(|cache| {
            // Cheap work (cloning the map) done INSIDE rcu
            let mut cache = HashMap::clone(cache);
            cache.insert(x, result);
            cache
        });
        result
    }
  6. Configure the HybridStrategy via the Config trait

    master

    The HybridStrategy is generic over a Cfg type that must implement the Config trait. This allows users to control the behavior of the strategy, specifically whether to enable the fast path.

    To use the default configuration (which enables the fast path), use DefaultConfig.

    To disable the fast path (primarily for testing purposes), implement a custom Config where USE_FAST is false.

  7. Handle optional values with `ArcSwapOption`

    master

    If you need to manage a value that can be empty (null), use ArcSwapOption. This allows you to store, swap, or compare_and_swap using Option<Arc<T>>. This is useful for representing state that can transition to a 'None' or 'null' state atomically.

    let shared = ArcSwapOption::from(Some(Arc::new(0)));
    let orig = shared.swap(None);
    assert_eq!(1, Arc::strong_count(&orig.unwrap()));
    
    let null = shared.load();
    assert!(null.is_none());
  8. Convert HybridProtection to an owned value with into_inner()

    master

    The HybridProtection<T> guard provides access to the protected value. To consume the guard and obtain the underlying owned value (which can outlive the ArcSwap it originated from), use the into_inner() method.

    Calling into_inner() will:

    1. Drop any outstanding debts.
    2. Increment the reference count if necessary to ensure the pointer remains valid.
    3. Return the full-featured value T.
  9. Load values using `load` and `load_full`

    master

    ArcSwap provides two primary ways to access the underlying data:

    1. load(): Returns a Guard. This is a lightweight lease. Depending on the internal strategy and thread-local state, it may not increment the Arc reference count immediately to optimize performance.
    2. load_full(): Returns a Guard that contains a full Arc clone. This ensures the reference count is incremented, making it safer for long-lived access or when passing the value across thread boundaries where the lease might expire.

    When the Guard is dropped, the underlying value's reference count is managed accordingly.

  10. Use `rcu` for atomic Read-Copy-Update operations

    master

    The rcu method allows you to perform an atomic Read-Copy-Update operation on the value inside the ArcSwap. You provide a closure that receives a reference to the current value. The closure can return a new value to be stored. This is useful for complex updates that depend on the current state. If the closure panics, the ArcSwap remains unchanged.

    Note that rcu can be called recursively, but be mindful of the logic within the closure to avoid infinite loops or deadlocks.

    let shared = ArcSwap::from(Arc::new(0));
    
    shared.rcu(|i| {
        if **i < 10 {
            shared.rcu(|i| **i + 1);
        }
        **i
    });
    assert_eq!(10, **shared.load());
  11. Use ArcSwapOption for atomic Option<Arc> storage

    master

    API

    ArcSwapOption<T> is a type alias for ArcSwapAny<Option<Arc<T>>>. Use this when you need to store a value that might be None (NULL).

    Common Operations

    • from_pointee(val): Creates a new ArcSwapOption from a value that can be converted into Option<T>. It allocates the Arc automatically.
    • empty(): Creates an ArcSwapOption holding None.
    • const_empty(): A const version of empty() for use in static declarations.
    • load_full() / load() / swap() / store(): These behave identically to ArcSwap, but operate on Option<Arc<T>>.
    use std::sync::Arc;
    use arc_swap::ArcSwapOption;
    
    let shared = ArcSwapOption::from(None);
    assert!(shared.load_full().is_none());
    assert!(shared.swap(Some(Arc::new(42))).is_none());
    assert_eq!(42, **shared.load_full().as_ref().unwrap());
  12. Use ArcSwap for atomic Arc storage

    master

    API

    ArcSwap<T> is a type alias for ArcSwapAny<Arc<T>>. It is the most common way to use the crate when you want to store an Arc and swap it atomically.

    Common Operations

    • from_pointee(val): Creates a new ArcSwap containing an Arc of the provided value.
    • load(): Returns a Guard that provides a temporary, cheap borrow of the contained value. This is ideal for local variables on the stack.
    • load_full(): Returns the held value (cloning the Arc). This is more expensive than load() but useful if you need ownership or a longer-lived handle.
    • store(val): Replaces the current value with a new Arc.
    • swap(new): Replaces the value and returns the previous Arc.
    • rcu(f): Performs a Read-Copy-Update. It takes a closure f that operates on the current value and returns a new version. It automatically retries if another thread updates the value during the operation.
    use std::sync::Arc;
    use arc_swap::ArcSwap;
    
    let arc = Arc::new(42);
    let arc_swap = ArcSwap::from(arc);
    assert_eq!(42, **arc_swap.load());
    
    // Put a new one in there
    let new_arc = Arc::new(0);
    assert_eq!(42, *arc_swap.swap(new_arc));
    assert_eq!(0, **arc_swap.load());