#lock-free #circular-buffer #buffer #mmap #async #no-alloc

no-std mutringbuf

A lock-free single-producer, single-consumer (SPSC) ring buffer with in-place mutability, asynchronous support, and virtual memory optimisation

10 unstable releases (4 breaking)

new 0.5.1 Apr 17, 2025
0.4.2 Feb 7, 2025
0.4.0 Nov 15, 2024
0.3.1 Jun 27, 2024
0.1.3 Mar 29, 2024

#179 in Asynchronous

Download history 95/week @ 2025-01-25 90/week @ 2025-02-01 50/week @ 2025-02-08 12/week @ 2025-02-15 1/week @ 2025-02-22 1/week @ 2025-03-01 167/week @ 2025-04-12

167 downloads per month

MIT/Apache

110KB
2K SLoC

MutRingBuf

crates.io Documentation Rust + Miri

A lock-free single-producer, single-consumer (SPSC) ring buffer with in-place mutability, asynchronous support, and virtual memory optimisation.

Performance

Benchmarks indicate that ringbuf may outperform this crate in certain operations. However, my own tests using Instant suggest that mutringbuf is slightly faster. I recommend trying both to see which one meets your needs better.

Purpose

This crate was developed for real-time audio stream processing. You can find a simple example here. For instructions on running it, jump to the Tests, Benchmarks, and Examples section.

Features

  • default: Enables the alloc feature.
  • alloc: Uses the alloc crate for heap-allocated buffers.
  • async: Provides support for async/await.
  • vmem: Enables virtual memory optimisations.

vmem Extension

An interesting optimisation for circular buffers involves mapping the underlying buffer to two contiguous regions of virtual memory. More information can be found here.

This crate supports this optimisation through the vmem feature, which can only be used with heap-allocated buffers and is currently limited to unix targets. The buffer size must be a multiple of the system's page size (usually 4096). When using the default and new_zeroed methods, the correct size is calculated based on the provided minimum size. However, when using the from methods, the user must ensure this requirement is met to avoid panics.

At the moment, the feature has been tested on GNU/Linux, Android and iOS.

Usage

Note on Uninitialised Items

This buffer can handle uninitialised items, which can occur when the buffer is created with new_zeroed methods or when an initialised item is moved out via ConsIter::pop or AsyncConsIter::pop.

As noted in the ProdIter documentation, there are two ways to push an item into the buffer:

  • Normal methods can only be used when the target location is initialised.
  • *_init methods must be used when the target location is uninitialised.

Using normal methods on uninitialised values can lead to undefined behaviour (UB), such as a segmentation fault (SIGSEGV).

Initialising Buffers and Iterators

First, create a buffer. Local buffers are generally faster due to the use of plain integers as indices, but they are not suitable for concurrent environments. In some cases, concurrent buffers may perform better than local ones.

Stack-Allocated Buffers

use mutringbuf::{ConcurrentStackRB, LocalStackRB};

// Buffers filled with default values
let concurrent_buf = ConcurrentStackRB::<usize, 4096>::default();
let local_buf = LocalStackRB::<usize, 4096>::default();

// Buffers built from existing arrays
let concurrent_buf = ConcurrentStackRB::from([0; 4096]);
let local_buf = LocalStackRB::from([0; 4096]);

// Buffers with uninitialised (zeroed) items
unsafe {
    let concurrent_buf = ConcurrentStackRB::<usize, 4096>::new_zeroed();
    let local_buf = LocalStackRB::<usize, 4096>::new_zeroed();
}

Heap-Allocated Buffers

use mutringbuf::{ConcurrentHeapRB, LocalHeapRB};

// Buffers filled with default values
let concurrent_buf: ConcurrentHeapRB<usize> = ConcurrentHeapRB::default(4096);
let local_buf: LocalHeapRB<usize> = LocalHeapRB::default(4096);

// Buffers built from existing vectors
let concurrent_buf = ConcurrentHeapRB::from(vec![0; 4096]);
let local_buf = LocalHeapRB::from(vec![0; 4096]);

// Buffers with uninitialised (zeroed) items
unsafe {
    let concurrent_buf: ConcurrentHeapRB<usize> = ConcurrentHeapRB::new_zeroed(4096);
    let local_buf: LocalHeapRB<usize> = LocalHeapRB::new_zeroed(4096);
}

Buffer Usage

The buffer can be utilised in two primary ways:

Sync Immutable

This is the standard way to use a ring buffer, where a producer inserts values that will eventually be consumed.

use mutringbuf::{LocalHeapRB, HeapSplit};

let buf = LocalHeapRB::from(vec![0; 4096]);
let (mut prod, mut cons) = buf.split();

Sync Mutable

Similar to the immutable case, but with an additional iterator work that allows for in-place mutation of elements.

use mutringbuf::{LocalHeapRB, HeapSplit};

let buf = LocalHeapRB::from(vec![0; 4096]);
let (mut prod, mut work, mut cons) = buf.split_mut();

Async Immutable

use mutringbuf::LocalHeapRB;

let buf = LocalHeapRB::from(vec![0; 4096]);
let (mut as_prod, mut as_cons) = buf.split_async();

Async Mutable

use mutringbuf::LocalHeapRB;

let buf = LocalHeapRB::from(vec![0; 4096]);
let (mut as_prod, mut as_work, mut as_cons) = buf.split_mut_async();

Iterators can also be wrapped in a Detached or an AsyncDetached, allowing for exploration of produced data back and forth while indirectly pausing the consumer.

Each iterator can be passed to a thread to perform its tasks. More information can be found in the respective documentation pages:

Note that a buffer, regardless of its type, remains alive until the last of its iterators is dropped.

Tests, Benchmarks, and Examples

Miri tests can be found within the script directory. The following commands should be run from the root of the crate.

To run tests:

cargo +nightly test

To run benchmarks:

cargo bench

To run the CPAL example:

RUSTFLAGS="--cfg cpal" cargo run --example cpal

If you encounter an error like: ALSA lib pcm_dsnoop.c:567:(snd_pcm_dsnoop_open) unable to open slave, please refer to this issue.

To run the async example:

cargo run --example simple_async --features async

Every other example_name can be run with:

cargo run --example `example_name` 

Dependencies

~120KB