Context: Real-time embedded ARM microcontroller.
Writing a lock-free, wait-free SPSC ring buffer is straight forward if the data in the buffer can be read or written atomically. But, in many cases, the buffer needs to be read over many ops, or written through many ops. How can a SPSC lock-free ring buffer be implemented? (The goal is for the ring-buffer to overwrite oldest data on overflow, not block.)
Imagine that the read of entry N is in process. Now, the write pointer catches up to N. If the read of N hadn't started, we'd simply overwrite N, and advance the read ptr to N+1; N becomes the last entry (most recently written) and N+1 the first entry (least recently written). But, now, since the read is in progress, we don't want to overwrite N. Instead, we need to make the write ptr N + 1 and the read ptr N + 2.
Likewise, if the write ptr is at N, and the read ptr advances to N-1. If the write to N-1 is complete, we can read N-1. But what if the write is in progress.
It seems like in addition to write ptr and read ptr, we also need to know if write is in progress and read is in progress. However, using those two extra information to construct invariants that keep the ring buffer correct is very difficult.
Questions:
For a lock-free, wait-free, SPSC ring buffer with overflow, that does not allow reading an item until write is complete, and does not allow overwriting an item that is in progress of reading:
- What data do we need (besides read ptr and write ptr)?
- What invariants apply to that data?
- How do we use that to construct the buffer?
I'm not sure what exactly you're looking for and it's not clear what you mean by non-atomic reads/writes. However, I think you can implement such a data structure using sequence locks.
Both the reader and the writer maintain their own current item pointers.
Each item has an associated sequence number, which is initially zero. The writer first increments the sequence number of the item it wishes to overwrite (setting some odd value). Then it modifies the item. Finally, the writer increments the sequence number one more time (setting it to the next even value), and progresses to the next item.
On the other hand the reader starts by checking the sequence number of the current item. If the number is odd, the item is being modified right now, so the reader goes to item no. N+1. Otherwise, the reader makes a copy of the item's data and then checks the sequence number one more time. If the number didn't change in the meantime, the reader reads the copy made a moment ago. If it did change, the reader discards the copy and goes to item no. N+1.
Additionally, if the sequence number is even, the reader may also check if it's not too big, which can occur when the writer has advanced past the reader and already finished writing. For that the reader keeps an epoch number, initially two (assuming zero means an empty item), and increases it by two after each full buffer was read. The reader only reads items whose sequence number is equal to the current epoch, and progresses forward to the next item every time it sees an item from the next epoch too early.
This is a little bit different than what you requested, because you said you don't want to interrupt the reader, but want the writer to skip to N+1 directly when a read is in progress, but I don't understand what's the point of this requirement. A read of a single item can be considered to be executed instantaneously if the copy of data is already made, so it cannot be interrupted. If the approach given above does not satisfy your goals, please provide more details about what you expect from the buffer (besides it being non-blocking, and the writer overwriting on overflow).