Skip to content
Daniel Esambu
Work
Work
Projects
Things I've built
Leka
A C++20 matching engine
Latency Lab
(opens in a new tab)
A C++ course I wrote
Resume
The formal version
Writing
Writing
Poetry
Poems, shelved by what they're about
Essays
Notes and essays
Math
Math
Notes
Notes I've written up
IB papers
Worked exam questions
Calculus lessons
(opens in a new tab)
On YouTube
Life
Life
Photos
Things I stopped to look at
Training
Reps, not phases
Modeling
Measurements and photos
Resume
Say hi
Say hi
Resume
Menu
Leka
A low-latency C++20 price-time-priority limit order book and matching engine
Replay viewer
Toggle main menu visibility
Loading...
Searching...
No Matches
spsc_event_queue.hpp
1
// include/lob/concurrency/spsc_event_queue.hpp
2
#ifndef SPSC_EVENT_QUEUE_HPP
3
#define SPSC_EVENT_QUEUE_HPP
4
5
#include "
lob/order/order_event.hpp
"
6
7
#include <array>
8
#include <atomic>
9
#include <cstddef>
10
#include <memory>
11
#include <new>
12
13
namespace
lob {
14
15
/**
16
* @brief Wait-free single-producer/single-consumer queue of OrderEvent.
17
*
18
* This is the boundary between a feed-handler thread (decoding or generating
19
* events) and the matching thread (calling MatchingEngine::processEvent),
20
* mirroring the two-thread split a real venue-facing system uses instead of
21
* doing both jobs on one thread. See ARCH_DECISIONS.md ADR-009 for why this
22
* shape, and measured cross-thread hand-off latency.
23
*
24
* Producer and consumer must each be called from exactly one thread apiece,
25
* always the SAME thread for the lifetime of the queue — tryPush() from more
26
* than one thread, or tryPop() from more than one thread, is undefined
27
* behavior. That restriction is what makes this SPSC rather than MPMC, and
28
* is also what makes it possible to implement with no compare-and-swap loop
29
* at all: each side has exactly one writer for its own cursor, so a plain
30
* atomic store, correctly ordered, is enough. No lock, no CAS retry loop,
31
* and no blocking: tryPush()/tryPop() either succeed immediately or report
32
* "not right now" and return, which is what "wait-free" means here.
33
*
34
* head_ and tail_ are each on their own 64-byte cache line
35
* (std::hardware_destructive_interference_size on most platforms actually
36
* targeted, but the exact value is not guaranteed portable, so this uses the
37
* conventional 64 directly). Without that padding, the producer's tail_
38
* store and the consumer's head_ store would share a cache line, and every
39
* single push and pop would force that line to bounce between the two
40
* cores' caches (MESI/MOESI invalidation) even though the two threads never
41
* touch the same logical field — "false sharing," and a real, measurable
42
* throughput cost specifically because there IS no lock here to hide it
43
* behind.
44
*
45
* Storage is raw, placement-constructed bytes rather than
46
* std::array<OrderEvent, Capacity>, the same technique OrderPool
47
* (include/lob/book/order_pool.hpp) already uses for Order: OrderEvent has
48
* no default constructor by design (see order_event.hpp), and a
49
* std::array of it would require one.
50
*/
51
template
<std::
size_t
Capacity>
52
class
SpscEventQueue {
53
static_assert
((Capacity & (Capacity - 1)) == 0,
"Capacity must be a power of two"
);
54
static_assert
(Capacity >= 2,
"Capacity must be at least 2"
);
55
56
public
:
57
SpscEventQueue() =
default
;
58
SpscEventQueue(
const
SpscEventQueue&) =
delete
;
59
SpscEventQueue& operator=(
const
SpscEventQueue&) =
delete
;
60
SpscEventQueue(SpscEventQueue&&) =
delete
;
61
SpscEventQueue& operator=(SpscEventQueue&&) =
delete
;
62
63
/** Destroys any events left in the queue at the time of destruction. */
64
~SpscEventQueue
() {
65
std::size_t head = head_.load(std::memory_order_relaxed);
66
const
std::size_t tail = tail_.load(std::memory_order_relaxed);
67
for
(; head != tail; ++head) {
68
std::destroy_at(slot(head));
69
}
70
}
71
72
/**
73
* @brief Producer-only. Appends @p event if the queue is not full.
74
* @return false if the queue was full; @p event is left untouched.
75
*/
76
bool
tryPush
(
const
OrderEvent
& event) {
77
const
std::size_t tail = tail_.load(std::memory_order_relaxed);
78
if
(tail - cachedHead_ >= Capacity) {
79
// Only re-read the consumer's cursor, an inter-core load,
80
// when our own stale copy says we might be full. Once
81
// refreshed, that copy is good for every push up to the
82
// point it was taken, so a producer draining into a
83
// consumer that is comfortably keeping up almost never
84
// pays this load at all.
85
cachedHead_ = head_.load(std::memory_order_acquire);
86
if
(tail - cachedHead_ >= Capacity) {
87
return
false
;
// genuinely full
88
}
89
}
90
std::construct_at(slot(tail), event);
91
// release: publishes both the new tail AND everything written
92
// above it (the constructed OrderEvent) to the consumer's
93
// subsequent acquire load of tail_.
94
tail_.store(tail + 1, std::memory_order_release);
95
return
true
;
96
}
97
98
/**
99
* @brief Consumer-only. Moves the next event into @p out if present.
100
* @return false if the queue was empty; @p out is left untouched.
101
*/
102
bool
tryPop
(
OrderEvent
& out) {
103
const
std::size_t head = head_.load(std::memory_order_relaxed);
104
if
(head == cachedTail_) {
105
cachedTail_ = tail_.load(std::memory_order_acquire);
106
if
(head == cachedTail_) {
107
return
false
;
// genuinely empty
108
}
109
}
110
OrderEvent
* eventSlot = slot(head);
111
out = std::move(*eventSlot);
112
std::destroy_at(eventSlot);
113
// release: publishes the freed slot to the producer's
114
// subsequent acquire load of head_ in tryPush()'s refresh path.
115
head_.store(head + 1, std::memory_order_release);
116
return
true
;
117
}
118
119
/**
120
* @brief Approximate occupancy, for diagnostics only.
121
*
122
* Reads both cursors without synchronizing them against each other,
123
* so the result can be stale or transiently negative-looking
124
* (wrapped) the instant either side is mid-operation. Never use
125
* this to decide whether tryPush()/tryPop() will succeed; call them
126
* and check their return value instead.
127
*/
128
std::size_t
sizeApprox
()
const
{
129
return
tail_.load(std::memory_order_relaxed) - head_.load(std::memory_order_relaxed);
130
}
131
132
static
constexpr
std::size_t capacity() {
return
Capacity; }
133
134
private
:
135
static
constexpr
std::size_t mask_ = Capacity - 1;
136
137
OrderEvent* slot(std::size_t index) {
138
return
std::launder(
reinterpret_cast<
OrderEvent*
>
(
139
&storage_[(index & mask_) *
sizeof
(OrderEvent)]));
140
}
141
142
// Every member below is deliberately on its own cache line. head_
143
// is written only by the consumer and read by the producer; tail_
144
// is written only by the producer and read by the consumer;
145
// cachedHead_ and cachedTail_ are each private to one side and
146
// never touched by the other thread at all, but still get their
147
// own line so that a write to one never invalidates a neighbor a
148
// future change might otherwise pack in beside it.
149
alignas
(64) std::atomic<std::size_t> head_{0};
150
alignas
(64) std::atomic<std::size_t> tail_{0};
151
alignas
(64) std::size_t cachedHead_{0};
// producer-private view of head_
152
alignas
(64) std::size_t cachedTail_{0};
// consumer-private view of tail_
153
alignas
(
alignof
(OrderEvent)) std::array<std::byte, Capacity *
sizeof
(OrderEvent)> storage_;
154
};
155
156
}
// namespace lob
157
158
#endif
// SPSC_EVENT_QUEUE_HPP
lob::OrderEvent
Type-safe event-first command for the matching engine.
Definition
order_event.hpp:77
lob::SpscEventQueue::~SpscEventQueue
~SpscEventQueue()
Destroys any events left in the queue at the time of destruction.
Definition
spsc_event_queue.hpp:64
lob::SpscEventQueue::tryPush
bool tryPush(const OrderEvent &event)
Producer-only.
Definition
spsc_event_queue.hpp:76
lob::SpscEventQueue::tryPop
bool tryPop(OrderEvent &out)
Consumer-only.
Definition
spsc_event_queue.hpp:102
lob::SpscEventQueue::sizeApprox
std::size_t sizeApprox() const
Approximate occupancy, for diagnostics only.
Definition
spsc_event_queue.hpp:128
order_event.hpp
Defines event-first order command payloads and dispatch types.
include
lob
concurrency
spsc_event_queue.hpp
Generated by
1.18.0