1. Problem It Solves
A single-producer/single-consumer queue assigns one index to each thread and uses acquire/release atomics to publish fixed-buffer slots without a mutex. It makes an important constraint visible instead of leaving readers to guess. This lesson keeps only the C++11 core that fits one focused day.
2. Prerequisites
The ideas from Day 50, plus basic variables, functions, and output already introduced.
3. Core Idea
Mental model: A single-producer/single-consumer queue assigns one index to each thread and uses acquire/release atomics to publish fixed-buffer slots without a mutex. Identify the relevant value or state, who owns it, and whether the rule acts during compilation or execution.
4. Minimal Syntax
tail.store(next, release); head.load(acquire); // one writer per index5. How It Works
The example creates a tiny fixed state with no keyboard input.
C++11 or the standard-library contract applies today's rule.
The program prints the important result so it can be checked against the source.
6. Common Mistakes
Using this design with multiple producers or consumers creates races because its ownership assumptions no longer hold.
7. When to Use It
Use it when exactly one producer and one consumer need a bounded low-latency channel.
Avoid it when it hides ownership, lifetime, type, ordering, or cost.
8. Simple Example
A four-slot ring publishes three integers from one producer; one consumer pops them in FIFO order using acquire/release indexes. The .cpp keeps the data fixed and avoids unrelated abstraction.
Complete sample code
Source file
cpp11/51_lock_free_spsc_queue/main.cpp
#include <array>
#include <atomic>
#include <cstddef>
#include <iostream>
#include <thread>
class SpscQueue {
public:
SpscQueue() : head_(0), tail_(0) {}
bool push(int value) {
const std::size_t tail = tail_.load(std::memory_order_relaxed);
const std::size_t next = (tail + 1) % data_.size();
if (next == head_.load(std::memory_order_acquire)) {
return false;
}
data_[tail] = value;
tail_.store(next, std::memory_order_release);
return true;
}
bool pop(int& value) {
const std::size_t head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire)) {
return false;
}
value = data_[head];
head_.store((head + 1) % data_.size(), std::memory_order_release);
return true;
}
private:
std::array<int, 4> data_;
std::atomic<std::size_t> head_;
std::atomic<std::size_t> tail_;
};
int main() {
SpscQueue queue;
std::thread producer([&] {
for (int value = 1; value <= 3; ++value) {
while (!queue.push(value)) {}
}
});
std::thread consumer([&] {
for (int count = 0; count < 3;) {
int value = 0;
if (queue.pop(value)) {
std::cout << value << (count == 2 ? '\n' : ' ');
++count;
}
}
});
producer.join();
consumer.join();
}
9. Key Takeaways
The feature is part of the C++11 scope used in this course.
Understand its lifetime, ownership, type, and ordering consequences.
Compile with warnings and prefer the smallest form that makes the rule obvious.
10. Self-Check Questions
Easy — In an SPSC queue with a fixed buffer, why should each index have only one writer? How do acquire/release operations let the producer and consumer exchange slots without a mutex?
Medium — Read the small example described above. What value or state should it print, and which rule produces that result?
Hard — Find and explain the subtle bug in this situation: Using this design with multiple producers or consumers creates races because its ownership assumptions no longer hold. What is the smallest C++11-safe correction?