Every time I search for "ringbuf" in lib.rs, the ringbuf crate always comes up first and it claims to be lock-free. When I take the time to read its documentation, I find that modifying it requires a mutable reference.
I don't quite understand why this library claims to be lock-free when it clearly doesn't support multithreading at all. If it did support multithreading, why would modification require a mutable reference?
If I'm understanding the docs at SharedRb in ringbuf::rb::shared - Rust correctly, you are supposed to use SharedRb and call .split() to get separate values for the reader and writer end of the ring buffer. You won't be able to push concurrently from multiple threads or consume concurrently from multiple threads, but you can move the reader and writer end to separate threads and use them concurrently.
ringbuf crate has two flavours of ring buffers. LocalRb and SharedRb. Are you sure you did not mistaken one from another? Can you point to specific part of the API, that you talk about?
if you have multiple producers and/or consumers, you need a concurrent queue, a bare ring buffer is not sufficient.
I suggest you check out the crossbeam crate instead, there are diffferent data structures, you can choose a suitable one for your use case. for example, I would guess ArrayQueue is likely what you need:
VecDeque is a ring buffer, so if you want something that works just like a Mutex<VecDeque> then you could just use a Mutex<VecDeque>?
A ring buffer is just a way of laying out a list in memory such that you can add/remove items from either end without having to move stuff around or allocate separate chunks of memory with pointers between them.
SharedRb in ringbuf uses atomic updates of the start/end pointers to make it possible to share the buffer between one producer and one consumer who are treating it like a FIFO queue, without a lock. Some people may assume this is what you mean by "ring buffer" because this is a very common use case and so people often pick a ring buffer specifically because it makes this kind of lock-free usage possible, and is simple and fast.
But, this simple atomic algorithm does not really generalize to multiple producers or multiple consumers - to support that without just using a lock, you need some more complex multi-step atomic update approach that requires tracking additional metadata about which parts of the buffer are in the process of being written to, and this just ends up being very different code to the SPSC version - that's probably why the ringbuf crate doesn't implement it. ArrayQueue from crossbeam linked above does use this kind of more complex atomic algorithm, though, if you want a lock-free implementation.
There are also multiple options for the semantics of an MPMC channel / ring buffer as well (options that dont really exist for SPSC queues).
Should it be broadcast (every reader gets every value) or should it each value be consumed by the first reader that reads it? (Or more exotic options like "last value only", you might miss out on intermediate values.)
What happens when a reader falls behind? Should it block writers or should the reader be notified that it lost some data (for the broadcast case)?
How are failures handled? I have seen APIs where you make an upfront reservation for pushing, and you are then guaranteed to be able to write that many bytes infallibly (for byte stream oriented channels).
Add to that all the usual options such as sync vs async vs both, mutex/lock free/wait free[1], allocating vs no-std friendly, etc. There is a huge design space for channels.
I have not seen wait free channels that are more than SPSC, but I dont know if that would be possible ↩︎
I think you are referring to the "bip buffer" api for a ring buffer. it is single producer single consumer only, and uses a two-step scheme for both the producer and consumer api.
for the producer, you first call the "reserve" (a.k.a. grant_write()) api, if it succeeds, you can write data to the reserved range, and then call commit to "publish" the data.
for the consumer, you first call a "grab" (a.k.a. grant_read()) api, if it succeeds, you get a range of the ring buffer containing valid data, and then call consume after you are done with it.
one aspect of this api design is to guarantee you can operate (both producer and consumer) on contiguous range of the buffer, which is important to certain use cases such as a DMA buffer in bare metal environment. the trade-off is a (typically very small) portion of the buffer's storage capacity could potentially be wasted.
see this blog post on one[1] rust implementation of the "bip buffer" idea, which was originally designed and implemented in C++.
in this bbqueue implementation, you get an exclusive slice (via Deref and DerefMut) from a successful grant operation, even for the consumer, which might seem unnecessary if you just need to read the data out of the buffer, but it can be handy if you need to process the mutate them in-place, e.g. sorting.
the rust implementation also has additional features not available in the original C++ version, such as byte stream oriented vs framed packet oriented apis, blocking/non-blocking/async apis.