Concurrent Queues, Ring Buffers, And Lock-Free Synchronization
Asked of: Software Engineer
Last updated

What's being tested
This topic tests whether you can reason about concurrent data structures rather than merely add a mutex around shared state. The interviewer is probing atomicity, ownership of memory locations, progress guarantees, memory ordering, bounded-capacity behavior, and correctness under contention. For a Software Engineer at Citadel, these skills matter because market-data and task-processing paths often demand both predictable latency and exact behavior when updates, consumers, and failures occur simultaneously.
Core knowledge
-
A ring buffer stores elements in fixed-capacity storage, using indices modulo capacity:
index = position % capacity. It avoids allocation and provides predictable memory usage, but must distinguish full from empty using counters, a reserved slot, or sequence numbers. -
For a single producer, only that thread advances the write position; consumers must atomically claim read positions, commonly with
compare_exchange_weak. Claiming before reading prevents two consumers from processing the same element. -
Publication requires correct memory ordering: the producer writes payload data, then performs a
store(..., release)on its publication marker; the consumer performs anacquireload before reading. Relaxed atomics alone do not publish payload writes. -
A lock-free algorithm guarantees system-wide progress: some thread completes in finitely many steps. Wait-free additionally bounds every individual operation. A mutex may be correct but blocking; lock-free does not mean starvation-free or automatically faster.
-
Sequence counters per slot solve both publication and reuse: a producer waits until a slot’s sequence indicates free, writes the payload, then advances the sequence; a consumer validates readiness before reading and marks the slot reusable afterward.
-
Capacity must account for outstanding work. With monotonic unsigned positions, occupancy is approximately
write_position - read_position; ensure counters cannot overflow ambiguously, or use sufficiently wide integers and carefully defined wraparound arithmetic. -
False sharing occurs when independent atomic counters share a cache line, causing cache-coherence traffic. Place producer and consumer positions on separate cache lines using padding or
alignas(64)where the target architecture justifies it. -
A queue’s progress policy must be explicit: spin, yield, block with
condition_variable, or return “full/empty.” Spinning can reducep99latency at low contention but wastes CPU and can starve lower-priority work. -
A conflating queue needs two structures: a map from symbol to its current entry and an ordered container of symbols awaiting delivery. Updating an existing symbol replaces its value without creating a duplicate, while new symbols preserve arrival order.
-
Sentinel values such as
-1are unsafe when negative prices are valid. Usestd::optional, a taggedstd::variant, or an explicit validity bit; define whether zero, negative values, and deletion are valid domain states. -
ABA occurs when a location changes from value A to B and back to A, making a compare-and-swap mistake an old state for an unchanged one. Version tags, per-slot sequence numbers, or ownership schemes reduce this risk.
-
Correctness includes destruction and exception behavior. Decide whether objects are constructed in-place, when destructors run, what happens if a consumer exits after claiming an item, and whether producers may overwrite storage containing an unread object.
Worked example
For “Implement a single-producer multi-consumer ring buffer”, I would first clarify whether each item is consumed exactly once, whether operations may block, whether overwriting is allowed, and whether the expected interface is C++-specific. I would state a bounded-capacity assumption, single producer ownership of writes, and that consumers compete to claim each item rather than broadcasting it to every consumer. I would organize the answer around storage and index layout, producer publication, consumer claim/read/release, and full/empty behavior. The producer would reserve or write the next slot, publish it with release semantics, and advance a sequence or write marker. Each consumer would use CAS to claim a unique read position, acquire the corresponding publication state, copy or move the payload, and make the slot reusable. The explicit tradeoff is between a simple CAS-based shared read index and per-slot sequence numbers: sequence numbers handle wraparound and producer/consumer visibility more robustly at the cost of metadata and complexity. I would mention cache-line padding and bounded spinning as latency considerations rather than hiding them inside correctness logic. If I had more time, I would add stress tests with randomized consumer scheduling, wraparound tests, ThreadSanitizer runs, and benchmarks for contention and p99 latency.
A second angle
For “Conflating Quote Queue That Keeps Only the Latest Price per Symbol”, the central issue shifts from claiming slots safely to defining update semantics under concurrency. A strong design uses a symbol-to-entry map plus an ordered queue, carefully marking whether a symbol is already enqueued so repeated updates do not create duplicate work. The candidate must clarify whether a consumer may observe an intermediate price and whether ordering means first arrival or most recent update. A mutex may be the best initial implementation; lock-free replacement is only worthwhile after measuring contention and latency.
Common pitfalls
Pitfall: Treating
volatileas synchronization is incorrect; it may inhibit some compiler optimizations but does not provide atomicity, inter-thread visibility, or ordering. Usestd::atomicand explain the happens-before relationship.
Pitfall: Saying “the ring buffer is lock-free because it uses atomics” skips the hard parts: multi-consumer index claiming, slot reuse, wraparound, and payload publication. Show who owns each mutation and what prevents overwrite or duplicate consumption.
Pitfall: Jumping directly to a complex lock-free algorithm without clarifying semantics signals poor engineering judgment. Start with a correct mutex-based or single-owner design, state its bottleneck, then justify each atomic optimization and its testing plan.
Connections
The interviewer may pivot to linearizability, memory reclamation such as hazard pointers or epoch-based reclamation, and backpressure under overload. They may also connect the queue to Kafka delivery semantics, priority queues, deadlock avoidance, or cache-coherence effects.
Further reading
-
C++ Concurrency in Action, 2nd Edition — practical treatment of atomics, memory ordering, lock-free structures, and thread coordination.
-
The Art of Multiprocessor Programming, 2nd Edition — rigorous coverage of linearizability, progress guarantees, CAS, ABA, and concurrent queues.
-
cppreference:
std::memory_order— precise reference for C++ acquire/release, relaxed ordering, and synchronization guarantees.
Practice questions
- Thread-Safe Producer-Consumer Queue: Mutex, Then Spin Lock, Then Lock-FreeCitadel Securities · Software Engineer · Onsite · medium
- Conflating Quote Queue That Keeps Only the Latest Price per SymbolCitadel Securities · Software Engineer · Onsite · medium
- Design a Thread-Safe Shared CounterCitadel · Software Engineer · Technical Screen · medium
- Implement a single-producer multi-consumer ring bufferCitadel · Software Engineer · Technical Screen · medium
- Implement task queue with insert, delete, executeCitadel · Software Engineer · Onsite · medium
- Discuss queues, NoSQL, and concurrencyCitadel · Software Engineer · Technical Screen · hard
Related concepts
- Thread-Safe Queues And Concurrency PrimitivesCoding & Algorithms
- Concurrency And SynchronizationSoftware Engineering Fundamentals
- Concurrency And Thread SafetyCoding & Algorithms
- Concurrency, OS Threads, And MemorySoftware Engineering Fundamentals
- Concurrency, Deadlocks, And SynchronizationSoftware Engineering Fundamentals
- Concurrency Control And Thread SafetySystem Design