2026-07-17
Stack Overflow: View Question
Tags: c++, multithreading, qt, operating-system, producer-consumer
Score: 0 | Views: 155
The asker has built a QIODevice wrapper around a background producer thread. The producer pulls data from an upstream source into a circular buffer; the consumer (the GUI thread) reads from it via the standard Qt API. To make seek() cheap without re-reading, they maintain a secondary circular index of buffer offsets, and they want the whole seek path to be lock-free.
Why it's harder than a textbook SPSC queue. A classic single-producer/single-consumer ring buffer is lock-free because there is exactly one writer of head and one writer of tail. Seeking breaks that clean split: the consumer wants to reposition its read cursor to an offset that may be anywhere inside the buffer, and it must simultaneously validate that (a) the offset is still resident (the producer hasn't overwritten it) and (b) the sub-index it consulted describes the buffer as it stands right now. Between reading the index and moving the cursor, the producer can advance head and invalidate everything.
A workable direction. Treat the problem as a versioned snapshot rather than a lock. Roughly:
memory_order_release after every append. The consumer's read cursor is expressed in the same absolute coordinate space, not as a wrapped index.(absolute_offset, metadata) entries — also written by the producer and published with release semantics. The consumer scans it lazily.acquire; (2) compute the tail boundary (write_counter - buffer_capacity); (3) look up the target in the sub-index; (4) attempt to install the new read cursor with a compare_exchange, retrying if the tail has advanced past the target in the meantime. If the target has fallen out of the window, fail the seek and let the caller re-request from source.Gotchas. ABA on the sub-index itself is real — reusing slots without a generation counter will let a stale entry look valid. QIODevice is fundamentally synchronous, so a seek that races with overwrite has to have a defined failure mode; silently returning stale bytes is worse than returning an error. Also watch for torn reads on 64-bit counters on 32-bit platforms — use std::atomic<uint64_t>, not a plain member. Finally, Qt signals across threads are queued, so any "buffer advanced" notification the consumer relies on is inherently stale by the time it's processed — the atomic counter must be the source of truth, not the signal.
