Build a ring buffer

A fixed-capacity queue over one array that is allocated once, built in three stages: a spare slot to tell full from empty, a power-of-two mask with indices that only grow, then one producer and one consumer with no lock.

How does a ring buffer tell full from empty?

With two indices and nothing else, head equal to tail can mean either, so you add one fact. The classic answer keeps one slot empty: the buffer is full when moving tail forward would land it on head, so head equal to tail only ever means empty. The other answer lets both indices grow without wrapping and masks them only when a slot is read or written; then tail minus head is the number of items, and full is that difference equalling the capacity. A third answer keeps a separate count, which costs a field that both sides have to update.

Why make the capacity a power of two?

Because the slot for an index is then index & (capacity - 1), one AND instruction in place of a division. It also lets the indices grow for ever: in a systems language an unsigned index wraps at 2 to the 64, and since a power-of-two capacity divides that, the masked slot and the difference tail - head stay right across the wrap. The cost is that a request for 3 slots gets 4, so say that you round up, and report the real capacity back.

Should a full ring buffer refuse the push or overwrite the oldest item?

It depends on who can afford to lose what, so make it a policy chosen when the buffer is built and not a flag checked on each push. Refusing is right when every item matters, such as a queue of commands: the producer gets a False and decides whether to retry, drop, or slow down. Overwriting is right when only recent data matters, such as a log tail or a window of samples. If you overwrite, count what you dropped, because a silent drop is the failure a later reader cannot diagnose.

Why is a ring buffer safe with one producer and one consumer and no lock?

Because each index has exactly one writer. The producer is the only one that stores tail and the consumer is the only one that stores head; each reads the other side once per call and acts on a value that can only have become more favourable since. The producer writes the item into its slot first and stores tail second, so by the time the consumer can see the new tail the item is already there. In C, C++ or Rust that store is an atomic store with release ordering and the other side loads it with acquire, which is what stops the hardware reordering the two.

What breaks when two producers share a single-producer ring buffer?

Both can read the same tail before either stores it. They then write the same slot, one item on top of the other, and both store the same new tail, so two pushes report success and only one item is ever delivered. Nothing crashes and no index is out of range, which is why it survives light testing. The fixes are a lock around the producer side, a compare-and-swap loop to claim a slot, or one ring per producer with the consumer reading all of them.