|
Leka
A low-latency C++20 price-time-priority limit order book and matching engine
|
Wait-free single-producer/single-consumer queue of OrderEvent. More...
#include <spsc_event_queue.hpp>
Public Member Functions | |
| SpscEventQueue (const SpscEventQueue &)=delete | |
| SpscEventQueue & | operator= (const SpscEventQueue &)=delete |
| SpscEventQueue (SpscEventQueue &&)=delete | |
| SpscEventQueue & | operator= (SpscEventQueue &&)=delete |
| ~SpscEventQueue () | |
| Destroys any events left in the queue at the time of destruction. | |
| bool | tryPush (const OrderEvent &event) |
| Producer-only. | |
| bool | tryPop (OrderEvent &out) |
| Consumer-only. | |
| std::size_t | sizeApprox () const |
| Approximate occupancy, for diagnostics only. | |
Static Public Member Functions | |
| static constexpr std::size_t | capacity () |
Wait-free single-producer/single-consumer queue of OrderEvent.
This is the boundary between a feed-handler thread (decoding or generating events) and the matching thread (calling MatchingEngine::processEvent), mirroring the two-thread split a real venue-facing system uses instead of doing both jobs on one thread. See ARCH_DECISIONS.md ADR-009 for why this shape, and measured cross-thread hand-off latency.
Producer and consumer must each be called from exactly one thread apiece, always the SAME thread for the lifetime of the queue — tryPush() from more than one thread, or tryPop() from more than one thread, is undefined behavior. That restriction is what makes this SPSC rather than MPMC, and is also what makes it possible to implement with no compare-and-swap loop at all: each side has exactly one writer for its own cursor, so a plain atomic store, correctly ordered, is enough. No lock, no CAS retry loop, and no blocking: tryPush()/tryPop() either succeed immediately or report "not right now" and return, which is what "wait-free" means here.
head_ and tail_ are each on their own 64-byte cache line (std::hardware_destructive_interference_size on most platforms actually targeted, but the exact value is not guaranteed portable, so this uses the conventional 64 directly). Without that padding, the producer's tail_ store and the consumer's head_ store would share a cache line, and every single push and pop would force that line to bounce between the two cores' caches (MESI/MOESI invalidation) even though the two threads never touch the same logical field — "false sharing," and a real, measurable throughput cost specifically because there IS no lock here to hide it behind.
Storage is raw, placement-constructed bytes rather than std::array<OrderEvent, Capacity>, the same technique OrderPool (include/lob/book/order_pool.hpp) already uses for Order: OrderEvent has no default constructor by design (see order_event.hpp), and a std::array of it would require one.
Definition at line 52 of file spsc_event_queue.hpp.
|
inline |
Destroys any events left in the queue at the time of destruction.
Definition at line 64 of file spsc_event_queue.hpp.
|
inline |
Producer-only.
Appends event if the queue is not full.
event is left untouched. Definition at line 76 of file spsc_event_queue.hpp.
|
inline |
Consumer-only.
Moves the next event into out if present.
out is left untouched. Definition at line 102 of file spsc_event_queue.hpp.
|
inline |
Approximate occupancy, for diagnostics only.
Reads both cursors without synchronizing them against each other, so the result can be stale or transiently negative-looking (wrapped) the instant either side is mid-operation. Never use this to decide whether tryPush()/tryPop() will succeed; call them and check their return value instead.
Definition at line 128 of file spsc_event_queue.hpp.
|
inlinestaticconstexpr |
Definition at line 132 of file spsc_event_queue.hpp.