Rb - a thread-safe ring-buffer

I've just published rb my thread-safe single-producer-single-consumer ring-buffer.
It's quite possible that it works as SPMC too, but I haven't tested this yet.

Some key features:

  • thread-safe
  • blocking and non-blocking IO. Blocking IO methods use Condvars to prevent busy-wait loops.
  • no unsafe blocks
  • does not allow under- or overflow because of range checks before the write or read is executed

I also wrote a small benchmark that simulates the throughput of 1min. of 48kHz single channel audio. It took about 16ms on my machine (Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz) to push 2.8 million samples through the buffer.

7 Likes

Hello @klingt.net, at work we do "Speech stuff"(sadly we do not use Rust, only C++) and I showed to a colleague of mine that works a lot with these kind of mechanisms when interacting with the recognition engine or with the "user", the rb API.
He had the impression "clear()" is a bit to drastic(an all or nothing approach). He proposed also an option(but also keep the current one) to clear just a few chunks(if it is possible), so, let's say the read can't catch up because there is some load on the CPU currently, the buffer gets full, when it gets full, you get the error.
When you get the error, you drop and jump over 2 chunks and continue to write/read, in the hope this time it will catch up.
This can happen for example when you synthesize text, you rather drop a few chunks but keep the experience for the user(you deliver the rest without noticeable glitches in the audio except that chunks that were dropped).
Here is the API he is working with that mapped well with your project, I think it could be a good inspiration for what your crate does(if it wasn't already).

Hi @LilianMoraru,
thank you for your response, I'm always happy when someone takes a look at my work :slight_smile:
The current API is pretty standard and I must admit that I haven't thought about clearing only a chunk of data. It makes total sense to do this and it should not be hard to implement, therefore I will add a clear(cnt: usize) method with the next minor release.
At the moment I'm fighting against some aliasing problems with my wavetable oscillators, so the next rb release could take one or two days :wink:

UPDATE
Is something like clear_pending() useful as well? Where pending are those frames/samples that weren't read by the producer already.

@LilianMoraru I've just updated rb and added skip(cnt: usize) and skip_pending() methods to the ring buffer.

Nice.
I didn't understand initially what clear_pending() was intended to do, so I didn't know what to respond and forgot about it, sorry...
I looked at the code to understand what it does.
I think I might see an issue with skip_pending() but I might be wrong:
Let's say there is a write buffer overrun and at buffer overrun we call skip_pending() but then the read is faster:

1 [write full circle]          1 [write] [read]                        1 [write]
2 [read]   - skip_pending() -> 2                - read is faster ->    2 [read]
3                              3                                       3
4                              4                                       4

Is this intended? I observe that the data is also not cleaned(that's probably why it's called skip), so the read might go over the same data after a skip_pending(), at least that's how it seems to me at moment.

If the write and read pointers are showing to the same position in the buffer then there are no elements available to read ( a call to buffer.count() will return zero). This means that a call to a consumers read method will immediately return Ok(0) in this situation. The same goes for write, a call to the consumers write method will immediately return zero if the write pointer is exactly one position behind the read pointer and therefore cant write anything without overrunning unread elements. Those checks prevent any kind of buffer under- or overrun in rb.
The blocking versions of both read_blocking and write_blocking will block in this situation until there are either slots free or there is data to read.

PS: You observation is correct, skip and skip_pending are not clearing the skipped elements.

Ok, I understand how it works now.
Having controlled underruns and overruns is ok. Although, I guess the way you try to do it is also okay.
When you basically would get a write overrun, you can do skip([number]), although, you might not have the luxury to retry, so the write might be lost, hmm. That's probably why ALSA can go for overrun and notifies you.
Looking at the code I don't understand how write is notified that a skip happened.

1 [write full circle]                  1 [write]                                      1
2 [read]                               2                                              2 [how does write know that it can write over this?]
3       - nowhere to write, skip(1) -> 3 [read]  - still reading, write() executed -> 3 [read]
4                                      4                                              4

I guess depends on what you want to do with the ring buffer, the API in general is ok.
Although, when you want to have every write be recorded and only skip reads when an overrun occurs(that's usually the desired behavior with live audio streaming - the buffer is limited but you want every write to be recorded), the API might limit you on this part.

You could look at it this way: The idea of this RB is that it won't do overruns and underruns, it has its usecases, you decide if it fits your use case.

Althought, I think you can have both. write and read with overruns and underruns, write_blocking and read_blocking without overruns and underruns.

Sorry for my late answer.

write is not explicitly notified. The synchronization mechanisms should ensure that write sees the new read position (after the skip() call) before it starts writing.
Retries are not possible with the current API but my idea was that the client is responsible to implement such behaviour. A reader can easily use the last frames it has read again and call the ring buffers read after he has retried.

I've just released rb 0.2.0 with some performance improvements and one breaking API change. The skip methods are now implemented for the Consumer. The consumer's also got a get method which does not change the buffers state or with other words, a call to it does not change the read pointer.