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
167 downloads per month
110KB
2K
SLoC
MutRingBuf
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 thealloc
feature.alloc
: Uses thealloc
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:
-
Sync
-
Async
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