1. Problem It Solves
A single producer and single consumer may need a bounded low-latency channel without mutex blocking. A ring buffer with separately owned indices can implement this narrow concurrency contract.
This lesson reduces that broad problem to one fixed-input program so the language rule and its observable result can be checked independently.
2. Prerequisites
A C++17 compiler invoked with warnings enabled and the earlier lessons listed in the course order.
Know ring buffers, atomics, acquire/release publication, cache contention, and fixed-capacity tradeoffs.
3. Core Idea
Only the producer writes the head index and only the consumer writes the tail. Release stores publish a completed slot or completed removal; matching acquire loads prevent a side from reusing a slot before the other side's work is visible.
Keep the type, object lifetime, ownership, and evaluation boundary visible while reading the example; syntax is useful only when those semantics are understood.
4. Minimal Syntax
buffer[head] = value;
head_.store(next, std::memory_order_release);
const auto head = head_.load(std::memory_order_acquire);5. How It Works
A producer pushes four fixed integers into a bounded ring while one consumer pops exactly four.
One slot remains unused to distinguish full from empty, and failed operations yield until progress is possible.
The program prints
received: 10 20 30 40, giving a small test oracle that can be compared with the prediction made before compilation.
6. Common Mistakes
Adding a second producer or consumer breaks index ownership assumptions; wraparound, destruction, and non-trivial element lifetimes also need a more complete design.
A successful build is not proof of correct semantics. Recheck lifetimes, invalidation, ordering, error paths, and required headers or link flags for the real program.
7. When to Use It
Use this technique when topology is exactly one producer and one consumer, capacity is bounded, and latency measurements justify lock-free complexity.
Choose a simpler C++11/14 form when the C++17 rule does not improve safety, clarity, or measured performance for the supported toolchains.
8. Simple Example
The demonstration uses integers so slot lifetime is simple. Thread joins and fixed counts avoid a separate shutdown protocol.
The companion .cpp file has no input or external dependency. Predict the complete output, compile it, run it, then change one constant and explain the new result.
Complete sample code
Source file
cpp17/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 {
static_assert(Capacity > 1);
public:
bool push(T value) {
const auto head = head_.load(std::memory_order_relaxed);
const auto next = (head + 1) % Capacity;
if (next == tail_.load(std::memory_order_acquire)) return false;
buffer_[head] = value;
head_.store(next, std::memory_order_release);
return true;
}
bool pop(T& value) {
const auto tail = tail_.load(std::memory_order_relaxed);
if (tail == head_.load(std::memory_order_acquire)) return false;
value = buffer_[tail];
tail_.store((tail + 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, 5> queue;
const std::array<int, 4> sent{{10, 20, 30, 40}};
std::array<int, 4> 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 means system progress without a lock, not automatic wait-freedom, fairness, or superior performance.
C++17 mode must be selected explicitly; a newer compiler default can otherwise hide a portability error.
Warnings, deterministic examples, and small assertions turn a remembered rule into evidence.
Document any lifetime, ownership, synchronization, or allocation contract at the API boundary.
10. Self-Check Questions
Easy — What problem does A Lock-Free SPSC Queue address?
Medium — Why does a four-slot ring hold at most three queued values in this design?
Hard — Which happens-before edge protects reading a slot after the producer writes it?