Project 2: Thread-Safe Producer-Consumer Queue
Build a queue that safely connects producers and consumers without busy-waiting.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C++17 |
| Coolness Level | Genuinely Clever |
| Business Potential | Resume Gold |
| Prerequisites | C++ basics, RAII, basic threading |
| Key Topics | mutexes, condition variables, shutdown protocol, atomics |
1. Learning Objectives
- Build a multi-producer/multi-consumer queue with safe shutdown.
- Use condition variables correctly (no spurious wakeup bugs).
- Understand happens-before and data races.
- Validate concurrency with TSan and stress tests.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Mutexes and Locking Discipline
Description
Mutexes provide mutual exclusion. A disciplined locking policy is the difference between correctness and deadlocks.
Definitions & Key Terms
- Mutex: exclusive lock.
- Critical section: region protected by a mutex.
Mental Model Diagram (ASCII)
Thread A: lock -> modify -> unlock
Thread B: lock -> modify -> unlock
How It Works
- Acquire lock before touching shared data.
- Release lock as soon as possible.
- Never hold locks across slow operations.
Minimal Concrete Example
std::lock_guard<std::mutex> lock(m);
q.push(item);
Common Misconceptions
- “Locks are always slow” -> contention is the real problem.
Check-Your-Understanding Questions
- What happens if a lock is not released?
Where You’ll Apply It
- Queue push/pop critical sections
2.2 Condition Variables
Description
Condition variables allow threads to sleep until a predicate is true. They can wake spuriously, so you must re-check.
Definitions & Key Terms
- Predicate: condition to be true when waking.
- Spurious wakeup: wake without notify.
Mental Model Diagram (ASCII)
wait(cv, pred)
| releases lock
| sleeps
| re-locks on wake
How It Works
- Consumer locks mutex.
cv.wait(lock, pred)releases lock and sleeps.- On notify, thread wakes, re-locks, and checks predicate.
Minimal Concrete Example
cv.wait(lock, [&]{ return !q.empty() || shutdown; });
Common Misconceptions
- “notify always wakes exactly one” -> spurious wakeups can happen.
Check-Your-Understanding Questions
- Why is the predicate required?
Where You’ll Apply It
- Queue pop
2.3 Memory Ordering and Shutdown Flags
Description
Thread shutdown protocols often use atomics to avoid races between producers and consumers.
Definitions & Key Terms
- Atomic flag: lock-free state indicator.
- Happens-before: ordering of writes and reads.
Mental Model Diagram (ASCII)
producer sets shutdown -> consumers observe flag
Minimal Concrete Example
std::atomic<bool> shutdown{false};
Common Misconceptions
- “Atomic means everything is safe” -> only that variable is atomic.
Where You’ll Apply It
- Queue shutdown behavior
3. Project Specification
3.1 What You Will Build
A templated queue with blocking pop(), non-blocking try_pop(), and a clean shutdown() that wakes all consumers.
3.2 Functional Requirements
- Thread-safe
pushandpop. - Blocking
popwith condition variable. shutdown()releases all waiting consumers.
3.3 Non-Functional Requirements
- No busy waiting.
- Safe under multiple producers/consumers.
- Clean shutdown under load.
3.4 Example Usage / Output
ThreadSafeQueue<int> q;
q.push(42);
auto v = q.pop();
3.5 Data Formats / Schemas / Protocols
- Internal:
std::queue<T>,std::mutex,std::condition_variable,bool shutdown.
3.6 Edge Cases
shutdown()while queue still has items- consumer waits with no producers
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
cmake -S . -B build
cmake --build build
./build/queue_demo --items=5 --seed=7
3.7.2 Golden Path Demo
[PRODUCER] push 0
[CONSUMER] pop 0
[PRODUCER] push 1
[CONSUMER] pop 1
[QUEUE] shutdown
[CONSUMER] exit
3.7.3 Failure Demo
[FAIL] consumer did not exit on shutdown
4. Solution Architecture
4.1 High-Level Design
[Producers] -> [Queue + Mutex] -> [Condition Variable] -> [Consumers]
4.2 Key Components
| Component | Responsibility | Key Decisions | |—|—|—| | Mutex | protect queue | lock granularity | | Condition variable | block consumers | predicate design | | Shutdown flag | signal exit | atomic or guarded |
4.3 Data Structures
struct ThreadSafeQueue {
std::queue<T> q;
std::mutex m;
std::condition_variable cv;
bool shutdown;
};
4.4 Algorithm Overview
push: lock, push, unlock, notify.pop: lock, wait predicate, pop, return.
5. Implementation Guide
5.1 Development Environment Setup
cmake -S . -B build
cmake --build build
5.2 Project Structure
queue-safe/
├── src/queue.hpp
├── src/queue.cpp
├── tests/queue_tests.cpp
└── CMakeLists.txt
5.3 The Core Question You’re Answering
“How do I share data between threads without races or wasting CPU?”
5.4 Concepts You Must Understand First
- Mutex locking discipline
- Condition variable usage
- Data races and happens-before
5.5 Questions to Guide Your Design
- When do you hold the lock?
- What is the correct predicate for waiting?
- How do you wake all consumers on shutdown?
5.6 Thinking Exercise
Draw a timeline for two consumers waiting and a producer waking them. What happens on shutdown?
5.7 The Interview Questions They’ll Ask
- Why must
cv.waituse a predicate? - What is a data race?
- What is the difference between
notify_oneandnotify_all?
5.8 Hints in Layers
Hint 1: cv.wait(lock, pred) is the safe pattern.
Hint 2: set shutdown flag before notifying.
Hint 3: return std::optional<T> from pop.
5.9 Books That Will Help
| Topic | Book | Chapter | |—|—|—| | Condition variables | C++ Concurrency in Action | Ch. 4 | | Thread lifecycle | C++ Concurrency in Action | Ch. 2 | | Concurrency basics | The C++ Programming Language | Concurrency chapter |
5.10 Implementation Phases
Phase 1: Single Producer/Consumer (2-3 days)
- Basic queue with mutex.
- Checkpoint: passes basic tests.
Phase 2: Blocking Pop (2-3 days)
- Add condition variable.
- Checkpoint: no busy wait.
Phase 3: Shutdown (2-3 days)
- Implement shutdown flag + notify_all.
- Checkpoint: clean exit.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |—|—|—|—| | Shutdown flag | atomic / mutex-guarded | mutex-guarded | simplicity | | Pop result | throw / optional | optional | explicit shutdown handling |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |—|—|—| | Unit | single-thread behavior | push/pop | | Concurrency | multi-thread stress | 4 producers, 4 consumers | | Shutdown | exit logic | shutdown with empty queue |
6.2 Critical Test Cases
- Consumers block on empty queue.
- Shutdown wakes all consumers.
- No data races under TSan.
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|—|—|—|
| Missing predicate | spurious wakeup bug | use cv.wait(lock, pred) |
| Notify without lock | lost wakeups | hold lock when updating state |
| Shutdown race | consumers hang | set flag then notify_all |
7.2 Debugging Strategies
- Use TSan to catch races.
- Add log lines around wait/notify.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add
try_pop.
8.2 Intermediate Extensions
- Add bounded queue with backpressure.
8.3 Advanced Extensions
- Add lock-free variant (Michael-Scott queue).
9. Real-World Connections
9.1 Industry Applications
- Worker thread pools
- Task schedulers
9.2 Interview Relevance
- Condition variable and race condition questions
10. Resources
10.1 Essential Reading
- C++ Concurrency in Action (condvar, threads)
- The C++ Programming Language (concurrency chapter)
11. Self-Assessment Checklist
- I can explain why spurious wakeups occur.
- I can design a clean shutdown protocol.
- My queue passes TSan.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Blocking
popwith predicate - Clean shutdown
Full Completion:
- Stress tests pass
- No TSan warnings
Excellence:
- Bounded queue with backpressure