ringbuf

repository·master·Indexed 20 days ago

https://github.com/agerasev/ringbuf

A high-performance, lock-free SPSC (Single-Producer Single-Consumer) FIFO ring buffer library for Rust. Version 0.5.1 supports arbitrary item types and various storage backends, including heap-allocated (HeapRb), statically-allocated (StaticRb), and no_std/no_alloc environments. It provides direct access to inner data and supports batch operations and overwrite mode. Companion crates include async-ringbuf for runtime-agnostic async/await synchronization and ringbuf-blocking for blocking operations.

Tokens
17.6K
Snippets
56
Records
79
Agent score
68%

What's inside ringbuf

  1. Overview of ringbuf

    master

    ringbuf is a lock-free Single-Producer Single-Consumer (SPSC) FIFO ring buffer that provides direct access to its internal data. It is designed for high performance and flexibility, supporting arbitrary item types (not just Copy types) and various storage backends.

    Key features include:

    • Lock-free operations: Methods succeed or fail immediately without blocking.
    • Flexible Storage: Can be used with std and alloc, or in no_std/no_alloc environments using statically-allocated memory.
    • Batch Operations: Supports inserting and removing items one by one or in bulk.
    • Overwrite Mode: Supports overwriting the oldest elements when the buffer is full.
    • Async/Blocking Support: Available via derived crates async-ringbuf and ringbuf-blocking.
  2. Overview of async-ringbuf

    master

    async-ringbuf provides an async/await Single-Producer Single-Consumer (SPSC) FIFO ring buffer. It is built on top of the original ringbuf crate but adds asynchronous synchronization.

    Key features include:

    • Supports arbitrary item types (not limited to Copy).
    • Supports single-item or batch insertion and removal.
    • Thread-safe direct access to internal ring buffer memory.
    • Compatible with no_std and no_alloc environments.
    • Runtime agnostic: It does not depend on specific async runtimes like tokio or async-std.
  3. Choose the right ring buffer type

    master

    The library provides different buffer types depending on your threading model and memory requirements:

    Single-threaded

    • LocalRb: Optimized for single-threaded use. It is faster than SharedRb because it avoids CPU cache synchronization overhead.

    Multi-threaded (SPSC)

    • SharedRb: Can be shared between threads. Use this when a producer and consumer live on different CPU cores.
      • HeapRb: Recommended for most use cases. Stores contents in dynamic memory (requires alloc).
      • StaticRb: Stores contents in statically-allocated memory (useful for no_std or fixed-size requirements).

    Memory Management

    • Use HeapRb for dynamic sizing.
    • Use StaticRb for environments without a heap or when you need fixed-size static allocation.
  4. How AsyncProducer and AsyncConsumer work

    master

    The library provides AsyncProducer and AsyncConsumer traits which serve as the async/await analogs to the lock-free Producer and Consumer methods found in the core ringbuf crate.

    These traits allow you to asynchronously wait for specific events, such as:

    • The appearance of an item.
    • The availability of a free slot.
    • The completion of a full transfer of all items.
    • The closing of the opposite endpoint.
  5. Use the Frozen wrapper for deferred ring buffer updates

    master

    The Frozen wrapper provides a way to perform ring buffer operations (reading or writing) without immediately synchronizing the indices with the underlying ring buffer. Changes made via a Frozen wrapper are only visible to the opposite end (e.g., a producer's writes becoming visible to a consumer) when commit(), sync(), or drop() is called.

    Key Behaviors

    • FrozenProd<R>: A frozen write end. Items inserted are not visible to the consumer until commit() or sync() is called. Free space created by a consumer is not visible to the producer until sync() is called.
    • FrozenCons<R>: A frozen read end. Free space created by a consumer is not visible to the producer until commit() or sync() is called. Items inserted by a producer are not visible to the consumer until sync() is called.

    Synchronization Methods

    • commit(): Pushes local changes (write index for producers, read index for consumers) to the underlying ring buffer.
    • fetch(): Pulls updates from the underlying ring buffer (updates local read index for producers, or local write index for consumers).
    • sync(): Performs both commit() and fetch() to fully synchronize the local state with the ring buffer.
  6. Direct access wrappers for ring buffers

    master

    The Direct struct provides a way to wrap a ring buffer reference (RbRef) with specific access rights. It is used to create specialized handles for observing, producing, or consuming data. All changes made through these wrappers are synchronized with the ring buffer immediately.

    There are three primary type aliases for common access patterns:

    • Obs<R>: An Observer. Provides read-only access to metadata like capacity and indices, but cannot modify the buffer.
    • Prod<R>: A Producer. Provides write access to the buffer.
    • Cons<R>: A Consumer. Provides read access to the buffer.

    Note that Direct implements Drop, which automatically releases held read or write rights when the wrapper goes out of scope.

    use ringbuf::wrap::direct::{Direct, Obs, Prod, Cons};
    
    // Example usage patterns (conceptual):
    // let observer: Obs<R> = direct_wrapper.observe();
    // let producer: Prod<R> = ...;
    // let consumer: Cons<R> = ...;
  7. Understand the Storage trait for ring buffers

    master

    The Storage trait defines the abstraction for the underlying memory used by a ring buffer. Any type implementing Storage must provide a contiguous array of items.

    Safety Requirements for Implementers:

    • The storage must not alias with its contents (it must be safe to hold mutable references to the storage and its data simultaneously).
    • as_mut_ptr must point to the start of the underlying data.
    • len() must return a consistent value.

    Key Methods:

    • len(): Returns the total capacity/length of the storage.
    • as_ptr(): Returns a *const MaybeUninit<Self::Item> to the start of the storage.
    • as_mut_ptr(): Returns a *mut MaybeUninit<Self::Item> to the start of the storage.
    • slice(range): Returns a &[MaybeUninit<Self::Item>] for the specified range.
    • slice_mut(range): Returns a &mut [MaybeUninit<Self::Item>] for the specified range.
  8. Delegate Observer methods using DelegateObserver

    master

    The DelegateObserver trait is a helper for implementing wrapper types that need to expose the same interface as an underlying Observer. By implementing DelegateObserver for your type and specifying the Base observer, you can automatically inherit all Observer method implementations via a blanket implementation.

    This pattern is useful when you have a struct that wraps a ring buffer (or another observer) and you want to provide access to the buffer's state through your wrapper without manually re-implementing every method.

  9. Use LocalRb for single-threaded ring buffers

    master

    The LocalRb<S> struct provides a ring buffer implementation designed exclusively for single-threaded use. It is slightly faster than multi-threaded versions because it avoids cache synchronization overhead. It requires a type S that implements the Storage trait.

    Safety Requirements

    When using raw construction or destruction, you must adhere to the following safety invariants:

    • from_raw_parts: The items in the storage within the read..write range must be initialized, and items outside this range must be uninitialized. The read and write positions must be valid.
    • into_raw_parts: You are responsible for ensuring that the initialized contents of the storage are properly dropped after destructuring.
    /// Ring buffer for single-threaded use only.
    ///
    /// Slightly faster than multi-threaded version because it doesn't synchronize cache.
    pub struct LocalRb<S: Storage + ?Sized> {
  10. How PopIter works and how to commit changes

    master

    The PopIter struct is an iterator that removes items from the ring buffer as you iterate.

    Crucially, the producer will only see these items as removed when the iterator is either dropped or when commit() is explicitly called.

    If you want to ensure the read index is advanced immediately after a specific batch of processing, call commit() on the iterator. If the iterator is dropped without calling commit(), it will automatically call commit() in its Drop implementation.

    // Using PopIter to consume items
    let mut iter = rb.pop_iter();
    while let Some(item) = iter.next() { 
        // process item
    }
    // Items are committed here automatically via Drop
  11. How ring buffers and their components work together

    master

    A ring buffer in this crate is a lock-free Single-Producer Single-Consumer (SPSC) FIFO structure. It is composed of three main parts:

    1. Storage: A contiguous memory area where items are stored. The buffer can either own this storage or hold a mutable reference to it.
    2. Indices: Two indices, read and write.
      • read % capacity points to the oldest item.
      • write % capacity points to the next empty slot.
      • The indices use modulo 2 * capacity to distinguish between an empty buffer (read == write) and a full buffer ((write - read) % (2 * capacity) == capacity) without requiring an extra unused slot.
    3. Hold flags: Indicators that ensure only one producer and one consumer can access the buffer at a time.

    To use a ring buffer, you typically create an instance (like HeapRb) and then split() it into a pair of Producer and Consumer. The producer inserts items, and the consumer removes them.

  12. Manage local changes with Frozen::commit, fetch, and sync

    master

    When using a Frozen wrapper, you must manually manage the synchronization of indices to control when changes become visible to other parts of the system.

    • Use commit() to make your local index changes (like moving the write pointer after pushing items) visible to the other end.
    • Use fetch() to update your local view of the other end's index (like updating the read pointer to see how much space has been freed).
    • Use sync() to perform both operations simultaneously.

    Note: If the Frozen wrapper is dropped, it automatically calls commit() to ensure pending changes are not lost.