Thread-Safe Producer-Consumer Queue: Mutex, Then Spin Lock, Then Lock-Free
Citadel SecuritiesSoftware Engineer · OnsiteLast updated
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.
Implement a thread-safe queue through which producer threads hand work items to consumer threads, then make it progressively faster:
- a version protected by a mutex;
- the same queue with the mutex replaced by a spin lock;
- 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
popblock 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
- 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
- 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
poponce 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
- 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.
Hints
Hint 1
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.
Hint 2
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.
Hint 3
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.
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?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.