Katkat

Design a C++ Ring Buffer for Producer-Consumer Use

Applied IntuitionSoftware Engineer · Technical Screen

Last updated

This problem tests your understanding of multi-threaded synchronization primitives, memory ordering, and circular buffer logic in a low-latency environment.

Problem Statement

Design a thread-safe, fixed-capacity RingBuffer class in C++ suitable for a single-producer, single-consumer (SPSC) scenario. Your implementation should provide efficient push and pop operations without using heavy locks if possible.

Requirements

  • The buffer must have a fixed capacity defined at construction.
  • Implement bool try_push(const T& value): Returns false if the buffer is full, otherwise adds the element.
  • Implement bool try_pop(T& value): Returns false if the buffer is empty, otherwise retrieves the element.
  • Use atomic variables to manage head and tail pointers.
  • Use appropriate memory ordering (e.g., std::memory_order_acquire/release) to ensure thread safety without excessive synchronization overhead.

Interface

template 
class RingBuffer {
public:
    explicit RingBuffer(size_t capacity);
    bool try_push(const T& value);
    bool try_pop(T& value);
private:
    // Add your members here
};

Constraints

  • The buffer size should be a power of two to optimize the wraparound logic using bitwise masks.
  • T is assumed to be trivially copyable for this implementation.

Example

RingBuffer rb(4);
rb.try_push(10); // Buffer: [10, _, _, _]
rb.try_push(20); // Buffer: [10, 20, _, _]
int val;
rb.try_pop(val); // Returns true, val = 10

Edge Cases

  • Pushing to a full buffer.
  • Popping from an empty buffer.
  • The state where head == tail (differentiate between empty and full).

Hints

Hint 1

To distinguish between empty and full states when head equals tail, you can either keep a counter or reserve one slot in the buffer that remains unused.

Hint 2

Using `std::atomic` for head and tail indices allows for lock-free communication between the producer and consumer.

Hint 3

Consider using `memory_order_release` when updating the tail in the producer and `memory_order_acquire` when reading the tail in the consumer to ensure visibility of the data.

Follow-up questions

  • How would your implementation change if you moved from an SPSC (single-producer single-consumer) model to an MPMC (multi-producer multi-consumer) model?
  • What are the performance implications of false sharing if your head and tail atomics reside on the same CPU cache line?
  • How would you handle non-trivially copyable types (e.g., `std::string`) while maintaining the lock-free performance characteristics?