1. Problem It Solves
A single producer and single consumer can exchange bounded items without a mutex when each side owns one index. An SPSC ring uses atomic head and tail publication plus fixed storage, avoiding allocation in the steady state.
Focus on the smallest useful form, its observable behavior, and its safety boundary.
2. Prerequisites
Days 32 and 43-45: ring buffers, atomics, release/acquire ordering, false sharing, and single-owner roles.
3. Core Idea
The producer alone writes tail and buffer slots before a release store. The consumer alone writes head after reading a slot, while acquire loads observe the other side's published progress.
Identify the objects and types, today's operation, and the printed result. This connects syntax to behavior.
4. Minimal Syntax
buffer[tail] = value;
tail.store(next, std::memory_order_release);
if (head != tail.load(std::memory_order_acquire)) { /* pop */ }5. How It Works
The producer retries bounded pushes for values 10, 20, and 30; only it updates the tail index.
The consumer retries pops, reads only published slots, and advances the head with release ordering.
After both threads join, the received array prints the original FIFO order without mutexes or dynamic allocation.
6. Common Mistakes
Using the same queue with multiple producers or consumers violates its ownership assumptions and creates races.
Do not copy the pattern without checking one-producer/one-consumer invariant, capacity-minus-one rule, index wraparound, publication order, object lifetime, and progress. A program may compile while still having the wrong lifetime, ownership, invalidation, ordering, or performance behavior.
7. When to Use It
Use it when exactly one producer and one consumer exchange small bounded data under measured latency requirements.
Avoid it when roles are multiple or dynamic, blocking is acceptable, or a proven library queue is available.
8. Simple Example
A capacity-four array exposes three usable slots to distinguish full from empty. Two threads transfer three integers and main prints the received values after joining.
The .cpp file uses fixed data. Predict its output, compile it, then change one value and test the prediction.
Complete sample code
Source file
cpp14/46_lock_free_spsc_queue/main.cpp
#include <array>
#include <atomic>
#include <cstddef>
#include <iostream>
#include <thread>
template <class T, std::size_t Capacity>
class SpscQueue {
public:
bool push(const T& value) {
const auto tail = tail_.load(std::memory_order_relaxed);
const auto next = (tail + 1) % Capacity;
if (next == head_.load(std::memory_order_acquire)) return false;
buffer_[tail] = value;
tail_.store(next, std::memory_order_release);
return true;
}
bool pop(T& value) {
const auto head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire)) return false;
value = buffer_[head];
head_.store((head + 1) % Capacity, std::memory_order_release);
return true;
}
private:
std::array<T, Capacity> buffer_{};
std::atomic<std::size_t> head_{0};
std::atomic<std::size_t> tail_{0};
};
int main() {
SpscQueue<int, 4> queue;
const std::array<int, 3> sent{{10, 20, 30}};
std::array<int, 3> received{};
std::thread producer([&] {
for (int value : sent)
while (!queue.push(value)) std::this_thread::yield();
});
std::thread consumer([&] {
for (int& value : received)
while (!queue.pop(value)) std::this_thread::yield();
});
producer.join();
consumer.join();
std::cout << "received:";
for (int value : received) std::cout << ' ' << value;
std::cout << "\n";
}
9. Key Takeaways
Lock-free SPSC correctness comes from fixed roles, index ownership, and precise publication ordering.
The producer alone writes
tailand buffer slots before a release store. The consumer alone writesheadafter reading a slot, while acquire loads observe the other side's published progress.The compiler or library follows a precise rule; verify one-producer/one-consumer invariant, capacity-minus-one rule, index wraparound, publication order, object lifetime, and progress.
Prefer the smallest form that communicates intent and measure costs when performance matters.
10. Self-Check Questions
Easy — What is the main purpose of A Lock-Free SPSC Queue?
Medium — Why does a ring with four physical slots expose only three usable queue positions in this design?
Hard — Which write must happen-before the consumer reads a slot, and how do release tail-store plus acquire tail-load establish that relation?