Lock-free ring buffers for SDR pipelines
An SDR receiver produces samples at a fixed rate whether you are ready or not. If the consumer thread stalls for a few milliseconds, the DMA engine keeps writing, and you either drop samples or block the producer. Neither is acceptable, so the queue between the two has to be boring and predictable.
For a single producer and a single consumer the classic design is enough: two indices, one written by each side, and acquire/release ordering on the handoff. The interesting parts are elsewhere:
- False sharing. Put the head and tail on separate cache lines, or the two cores spend their time bouncing one line back and forth.
- Cached indices. Each side keeps a local copy of the other side's index and only reloads it when the buffer looks full (or empty). This removes most cross-core traffic.
- Batching. Publish blocks of samples, not single samples. The per-item cost of an atomic store is small, but not when you do it 30 million times a second.
// producer
auto tail = tail_.load(std::memory_order_relaxed);
if (tail - cached_head_ == capacity) {
cached_head_ = head_.load(std::memory_order_acquire);
if (tail - cached_head_ == capacity) return false; // full
}
slots_[tail & mask] = block;
tail_.store(tail + 1, std::memory_order_release);
In practice a well-tuned off-the-shelf queue beats a hand-rolled one; the point of writing it once is to understand what the library is doing for you.