Thread-Safe Producer-Consumer Queue: Mutex, Then Spin Lock, Then Lock-Free

Read the full interview experience this question came from →

Quick Overview

Implement a thread-safe producer-consumer queue in C++, first with a mutex, then with a spin lock, then as a lock-free design with explicit memory ordering. Tests the C++ memory model, contention and cache effects, blocking versus spinning, and where memory is allocated on the hot path.

Thread-Safe Producer-Consumer Queue: Mutex, Then Spin Lock, Then Lock-Free

Company: Citadel Securities

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

Implement a thread-safe queue through which producer threads hand work items to consumer threads, then make it progressively faster: 1. a version protected by a mutex; 2. the same queue with the mutex replaced by a spin lock; 3. a lock-free version built on atomics, with an explicit memory order on every atomic operation. The discussion is in C++. Expect questions on memory ordering, on the standard containers you use (such as `std::queue` and `std::list`), on how those containers are implemented internally, and on where memory is allocated. ### Clarifying Questions - How many producers and how many consumers are there: exactly one of each, or several of each? - Is the queue bounded? If so, what should a producer do when it is full: block, fail, or overwrite? - Should `pop` block until an item arrives, or return immediately when the queue is empty? - What is the item type, and is it cheap to move? - How should the queue shut down, so that blocked threads wake up and exit? ### Part 1 — Mutex-based queue Implement `push` and `pop` protected by a `std::mutex`. A consumer that finds the queue empty must wait without burning CPU, and no item may ever be lost. ```hint Sleep on a condition A thread that cannot proceed should sleep until another thread changes the state it is waiting on. Decide what it waits on, and what it must re-check after it wakes. ``` #### What This Part Should Cover - A correct critical section: every read and write of shared state happens under the lock. - Waiting and waking: no lost wake-ups, and correct handling of spurious wake-ups. - Capacity and shutdown behavior that matches the clarified requirements. ### Part 2 — Replace the mutex with a spin lock Implement a spin lock on top of `std::atomic` and use it instead of the mutex. Explain when this is faster than the mutex version and when it is slower. ```hint Watch the lock's cache line Think about what every waiting thread does to the cache line that holds the lock flag, and how to wait while generating less traffic on it. ``` #### What This Part Should Cover - A spin lock whose acquire and release carry the right memory ordering. - Reducing contention and wasted work while spinning. - The conditions under which spinning beats sleeping, the failure mode when the lock holder is descheduled, and what happens to a blocking `pop` once there is no mutex to pair a sleep with. ### Part 3 — Lock-free queue and memory ordering Remove the lock entirely. Implement a lock-free queue for the producer and consumer topology you clarified, and justify the memory order of every atomic load and store. ```hint Count the writers For each shared variable, ask which threads write it. The fewer writers a variable has, the weaker the atomic operation it needs. ``` #### What This Part Should Cover - A correct publish and consume protocol: a consumer never reads an item before the producer has finished writing it, and a producer never overwrites a slot the consumer is still reading. - A justified choice between acquire/release, relaxed and sequentially consistent operations. - Hardware effects such as false sharing, and the extra problems (ABA, memory reclamation) that appear with several producers or consumers. ### What a Strong Answer Covers - Correctness first, then optimization, with a clear statement of what each step gains and what it gives up. - A precise explanation of the C++ memory model as it applies to this code, not only which keywords to use. - Where each version allocates memory, and why allocation inside a critical section or on the hot path matters. - How the three versions would be benchmarked and compared, including with more threads than cores. ### Follow-up Questions - What container does `std::queue` use underneath by default, and what allocations happen on `push` compared with a `std::list`-backed queue? - How would you extend the lock-free design to several producers and several consumers, and what is the ABA problem? - What happens to the spin-lock version if the thread holding the lock is preempted, and how can that be mitigated? - How would a consumer of the lock-free queue wait efficiently when the queue is empty?

Overview: Implement a thread-safe producer-consumer queue in C++, first with a mutex, then with a spin lock, then as a lock-free design with explicit memory ordering. Tests the C++ memory model, contention and cache effects, blocking versus spinning, and where memory is allocated on the hot path.

Read the full Citadel Securities Software Engineer interview experience this question came from

|Home/Software Engineering Fundamentals/Citadel Securities
Citadel Securities logo
Citadel Securities
Sep 11, 2026
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
0
0

Implement a thread-safe queue through which producer threads hand work items to consumer threads, then make it progressively faster:

  1. a version protected by a mutex;
  2. the same queue with the mutex replaced by a spin lock;
  3. a lock-free version built on atomics, with an explicit memory order on every atomic operation.

The discussion is in C++. Expect questions on memory ordering, on the standard containers you use (such as std::queue and std::list), on how those containers are implemented internally, and on where memory is allocated.

Clarifying Questions Guidance

  • How many producers and how many consumers are there: exactly one of each, or several of each?
  • Is the queue bounded? If so, what should a producer do when it is full: block, fail, or overwrite?
  • Should pop block until an item arrives, or return immediately when the queue is empty?
  • What is the item type, and is it cheap to move?
  • How should the queue shut down, so that blocked threads wake up and exit?

Part 1 — Mutex-based queue

Implement push and pop protected by a std::mutex. A consumer that finds the queue empty must wait without burning CPU, and no item may ever be lost.

What This Part Should Cover Guidance

  • A correct critical section: every read and write of shared state happens under the lock.
  • Waiting and waking: no lost wake-ups, and correct handling of spurious wake-ups.
  • Capacity and shutdown behavior that matches the clarified requirements.

Part 2 — Replace the mutex with a spin lock

Implement a spin lock on top of std::atomic and use it instead of the mutex. Explain when this is faster than the mutex version and when it is slower.

What This Part Should Cover Guidance

  • A spin lock whose acquire and release carry the right memory ordering.
  • Reducing contention and wasted work while spinning.
  • The conditions under which spinning beats sleeping, the failure mode when the lock holder is descheduled, and what happens to a blocking pop once there is no mutex to pair a sleep with.

Part 3 — Lock-free queue and memory ordering

Remove the lock entirely. Implement a lock-free queue for the producer and consumer topology you clarified, and justify the memory order of every atomic load and store.

What This Part Should Cover Guidance

  • A correct publish and consume protocol: a consumer never reads an item before the producer has finished writing it, and a producer never overwrites a slot the consumer is still reading.
  • A justified choice between acquire/release, relaxed and sequentially consistent operations.
  • Hardware effects such as false sharing, and the extra problems (ABA, memory reclamation) that appear with several producers or consumers.

What a Strong Answer Covers Guidance

  • Correctness first, then optimization, with a clear statement of what each step gains and what it gives up.
  • A precise explanation of the C++ memory model as it applies to this code, not only which keywords to use.
  • Where each version allocates memory, and why allocation inside a critical section or on the hot path matters.
  • How the three versions would be benchmarked and compared, including with more threads than cores.

Follow-up Questions Guidance

  • What container does std::queue use underneath by default, and what allocations happen on push compared with a std::list -backed queue?
  • How would you extend the lock-free design to several producers and several consumers, and what is the ABA problem?
  • What happens to the spin-lock version if the thread holding the lock is preempted, and how can that be mitigated?
  • How would a consumer of the lock-free queue wait efficiently when the queue is empty?
Loading comments...