Skip to content

Commit

Permalink
Add general description to the crate
Browse files Browse the repository at this point in the history
  • Loading branch information
agerasev committed Jan 17, 2024
1 parent de94874 commit e972ab0
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 47 deletions.
57 changes: 22 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,53 +24,45 @@ Lock-free SPSC FIFO ring buffer with direct access to inner data.
+ Items can be inserted and removed one by one or many at once.
+ Thread-safe direct access to the internal ring buffer memory.
+ `Read` and `Write` implementation.
+ Overwriting mode support.
+ Overwriting insertion support.
+ Different types of buffers and underlying storages.
+ Can be used without `std` and even without `alloc` (using only statically-allocated memory).
+ [Experimental `async`/`.await` support](./async).
+ Async and blocking versions (see [this section](#derived-crates)).

## Usage
# Usage

At first you need to create the ring buffer itself. `HeapRb` is recommended but you may [choose another one](#types).

After the ring buffer is created it may be splitted into pair of `Producer` and `Consumer`.
`Producer` is used to insert items to the ring buffer, `Consumer` - to remove items from it.
For `SharedRb` and its derivatives they can be used in different threads.
Producer is used to insert items to the ring buffer, consumer - to remove items from it.

## Types
# Types

There are several types of ring buffers provided:

+ `LocalRb`. Only for single-threaded use.
+ `SharedRb`. Can be shared between threads. Its derivatives:
+ `SharedRb`. Can be shared between threads. Its frequently used instances:
+ `HeapRb`. Contents are stored in dynamic memory. *Recommended for use in most cases.*
+ `StaticRb`. Contents can be stored in statically-allocated memory.

## Performance
You may also provide your own generic parameters.

`SharedRb` needs to synchronize CPU cache between CPU cores. This synchronization has some overhead.
To avoid multiple unnecessary synchronizations you may use postponed mode of operation (see description for `Producer` and `Consumer`)
or methods that operates many items at once (`Producer::push_slice`/`Producer::push_iter`, `Consumer::pop_slice`, etc.).

For single-threaded usage `LocalRb` is recommended because it is faster than `SharedRb` due to absence of CPU cache synchronization.

### Benchmarks
# Performance

You may see typical performance of different methods in benchmarks:

```bash
cargo +nightly bench --features bench
```
`SharedRb` needs to synchronize CPU cache between CPU cores. This synchronization has some overhead.
To avoid multiple unnecessary synchronizations you may use methods that operate many items at once
(`push_slice`/`push_iter`, `pop_slice`/`pop_iter`, etc.)
or you can `freeze` producer or consumer and then synchronize threads manually (see items in `frozen` module).

Nightly toolchain is required.
For single-threaded usage `LocalRb` is recommended because it is slightly faster than `SharedRb` due to absence of CPU cache synchronization.

## Examples

### Simple

```rust
use ringbuf::HeapRb;
use ringbuf::{traits::*, HeapRb};

# fn main() {
let rb = HeapRb::<i32>::new(2);
let (mut prod, mut cons) = rb.split();

Expand All @@ -85,15 +77,13 @@ prod.try_push(2).unwrap();
assert_eq!(cons.try_pop(), Some(1));
assert_eq!(cons.try_pop(), Some(2));
assert_eq!(cons.try_pop(), None);
# }
```

### No heap

```rust
use ringbuf::StaticRb;
use ringbuf::{traits::*, StaticRb};

# fn main() {
const RB_SIZE: usize = 1;
let mut rb = StaticRb::<i32, RB_SIZE>::default();
let (mut prod, mut cons) = rb.split_ref();
Expand All @@ -103,17 +93,15 @@ assert_eq!(prod.try_push(321), Err(321));

assert_eq!(cons.try_pop(), Some(123));
assert_eq!(cons.try_pop(), None);
# }
```

## Overwrite

Ring buffer can be used in overwriting mode when insertion overwrites the latest element if the buffer is full.

```rust
use ringbuf::{HeapRb, Rb};
use ringbuf::{traits::*, HeapRb};

# fn main() {
let mut rb = HeapRb::<i32>::new(2);

assert_eq!(rb.push_overwrite(0), None);
Expand All @@ -123,16 +111,15 @@ assert_eq!(rb.push_overwrite(2), Some(0));
assert_eq!(rb.try_pop(), Some(1));
assert_eq!(rb.try_pop(), Some(2));
assert_eq!(rb.try_pop(), None);
# }
```

Note that [`push_overwrite`](`Rb::push_overwrite`) requires exclusive access to the ring buffer
so to perform it concurrently you need to guard the ring buffer with [`Mutex`](`std::sync::Mutex`) or some other lock.
Note that `push_overwrite` requires exclusive access to the ring buffer
so to perform it concurrently you need to guard the ring buffer with mutex or some other lock.

## `async`/`.await`
## Derived crates

There is an experimental crate [`async-ringbuf`](https://gitlab.com/agerasev/async-ringbuf)
which is built on top of `ringbuf` and implements asynchronous ring buffer operations.
+ [`ringbuf-blocking`](https://crates.io/crates/ringbuf-blocking)
+ [`async-ringbuf`](https://crates.io/crates/async-ringbuf)

## License

Expand Down
147 changes: 146 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,151 @@
//! Lock-free SPSC FIFO ring buffer with direct access to inner data.
//!
//! For implementation details see [`RingBuffer`](`traits::RingBuffer`) description.
//! # Usage
//!
//! At first you need to create the ring buffer itself. [`HeapRb`] is recommended but you may [choose another one](#types).
//!
//! After the ring buffer is created it may be splitted into pair of [`Producer`](`traits::Producer`) and [`Consumer`](`traits::Consumer`).
//! Producer is used to insert items to the ring buffer, consumer - to remove items from it.
//!
//! # Types
//!
//! There are several types of ring buffers provided:
//!
//! + [`LocalRb`]. Only for single-threaded use.
//! + [`SharedRb`]. Can be shared between threads. Its frequently used instances:
//! + [`HeapRb`]. Contents are stored in dynamic memory. *Recommended for use in most cases.*
//! + [`StaticRb`]. Contents can be stored in statically-allocated memory.
//!
//! You may also provide your own generic parameters.
//!
//! # Performance
//!
//! [`SharedRb`] needs to synchronize CPU cache between CPU cores. This synchronization has some overhead.
//! To avoid multiple unnecessary synchronizations you may use methods that operate many items at once
//! ([`push_slice`](`traits::Producer::push_slice`)/[`push_iter`](`traits::Producer::push_iter`), [`pop_slice`](`traits::Consumer::pop_slice`)/[`pop_iter`](`traits::Consumer::pop_iter`), etc.)
//! or you can `freeze` [producer](`Prod::freeze`) or [consumer](`Cons::freeze`) and then synchronize threads manually (see items in [`frozen`](`wrap::frozen`) module).
//!
//! For single-threaded usage [`LocalRb`] is recommended because it is slightly faster than [`SharedRb`] due to absence of CPU cache synchronization.
//!
//! # Examples
//!
#![cfg_attr(
feature = "alloc",
doc = r##"
## Simple
```rust
use ringbuf::{traits::*, HeapRb};
# fn main() {
let rb = HeapRb::<i32>::new(2);
let (mut prod, mut cons) = rb.split();
prod.try_push(0).unwrap();
prod.try_push(1).unwrap();
assert_eq!(prod.try_push(2), Err(2));
assert_eq!(cons.try_pop(), Some(0));
prod.try_push(2).unwrap();
assert_eq!(cons.try_pop(), Some(1));
assert_eq!(cons.try_pop(), Some(2));
assert_eq!(cons.try_pop(), None);
# }
```
"##
)]
#![doc = r##"
## No heap
```rust
use ringbuf::{traits::*, StaticRb};
# fn main() {
const RB_SIZE: usize = 1;
let mut rb = StaticRb::<i32, RB_SIZE>::default();
let (mut prod, mut cons) = rb.split_ref();
assert_eq!(prod.try_push(123), Ok(()));
assert_eq!(prod.try_push(321), Err(321));
assert_eq!(cons.try_pop(), Some(123));
assert_eq!(cons.try_pop(), None);
# }
```
"##]
#![cfg_attr(
feature = "std",
doc = r##"
## Overwrite
Ring buffer can be used in overwriting mode when insertion overwrites the latest element if the buffer is full.
```rust
use ringbuf::{traits::*, HeapRb};
# fn main() {
let mut rb = HeapRb::<i32>::new(2);
assert_eq!(rb.push_overwrite(0), None);
assert_eq!(rb.push_overwrite(1), None);
assert_eq!(rb.push_overwrite(2), Some(0));
assert_eq!(rb.try_pop(), Some(1));
assert_eq!(rb.try_pop(), Some(2));
assert_eq!(rb.try_pop(), None);
# }
```
Note that [`push_overwrite`](`traits::RingBuffer::push_overwrite`) requires exclusive access to the ring buffer
so to perform it concurrently you need to guard the ring buffer with mutex or some other lock.
"##
)]
//!
//! # Implementation details
//!
//! Each ring buffer here consists of the following parts:
//!
//! + Storage
//! + Indices
//! + Hold flags
//!
//! ## Storage
//!
//! [`Storage`](`storage::Storage`) is a place where ring buffer items are actually stored.
//! It must span a single contiguous memory area (e.g. we can obtain a slice or subslice of it).
//! Ring buffer can own its storage or it can hold only a mutable reference to it.
//! Storage length is refered as `capacity`.
//!
//! ## Indices
//!
//! Ring buffer also contains two indices: `read` and `write`.
//!
//! `read % capacity` points to the oldest item in the storage.
//! `write % capacity` points to empty slot next to the most recently inserted item.
//!
//! When an item is extracted from the ring buffer it is taken from the `read % capacity` slot and then `read` index is incremented.
//! New item is put into the `write % capacity` slot and `write` index is incremented after that.
//!
//! Slots with indices between (in modular arithmetic) `(read % capacity)` (including) and
//! `(write % capacity)` (excluding) contain items (are initialized).
//! All other slots do not contain items (are uninitialized).
//!
//! The actual values of `read` and `write` indices are modulo `2 * capacity` instead of just `capacity`.
//! It allows us to distinguish situations when the buffer is empty (`read == write`)
//! and when the buffer is full (`(write - read) % (2 * capacity) == capacity`)
//! without using an extra slot in container that cannot be occupied.
//!
//! But this causes the existense of invalid combinations of indices.
//! For example, we cannot store more than `capacity` items in the buffer,
//! so `(write - read) % (2 * capacity)` is not allowed to be greater than `capacity`.
//!
//! ## Hold flags
//!
//! Ring buffer can have at most one producer and at most one consumer at the same time.
//! These flags indicates whether it's safe to obtain a new producer or a consumer.
//!
#![no_std]
#![allow(clippy::type_complexity)]
#![cfg_attr(feature = "bench", feature(test))]
Expand Down
11 changes: 0 additions & 11 deletions src/traits/ring_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,6 @@ use super::{
};

/// An abstract ring buffer that exclusively owns its data.
///
/// # Details
///
/// The ring buffer consists of an array (of `capacity` size) and two indices: `read` and `write`.
/// When an item is extracted from the ring buffer it is taken from the `read` index and after that `read` is incremented.
/// New item is appended to the `write` index and `write` is incremented after that.
///
/// The `read` and `write` indices are modulo `2 * capacity` (not just `capacity`).
/// It allows us to distinguish situations when the buffer is empty (`read == write`) and when the buffer is full (`write - read` modulo `2 * capacity` equals to `capacity`)
/// without using the space for an extra element in container.
/// And obviously we cannot store more than `capacity` items in the buffer, so `write - read` modulo `2 * capacity` is not allowed to be greater than `capacity`.
pub trait RingBuffer: Observer + Consumer + Producer {
/// Tell whether read end of the ring buffer is held by consumer or not.
///
Expand Down

0 comments on commit e972ab0

Please sign in to comment.