Skip to main content

low_latency_data_structures/spmc/
consumer.rs

1use std::marker::PhantomData;
2use std::sync::Arc;
3use std::sync::atomic::{Ordering, fence};
4
5use crossbeam_utils::CachePadded;
6
7use crate::mem::{Allocation, Allocator};
8use crate::spmc::queue::{Queue, Slot};
9
10pub(super) type ConsumerState = CachePadded<ConsumerStateInner>;
11
12#[derive(Clone)]
13pub(super) struct ConsumerStateInner {
14    read_cursor: usize,
15    cached_write_cursor: usize,
16}
17
18/// Outcome of a [`Consumer::try_read`] call.
19///
20/// `try_read` is non-blocking and never allocates, so the return value
21/// distinguishes three cases the caller must handle: a successful read, an
22/// empty queue, or a detected lap. The enum is `#[must_use]` because
23/// silently dropping `Value` would lose the only copy of the data, and
24/// silently dropping `Lapped` would hide message loss.
25#[must_use = "the returned ReadResult indicates whether a value was read, the queue was empty, or the consumer was lapped"]
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ReadResult<T> {
28    /// A value was successfully read from the next slot.
29    Value(T),
30    /// The queue is currently empty.
31    Empty,
32    /// The producer lapped this consumer at least once. The read cursor has
33    /// been advanced to the most recently published slot; `skipped` is the
34    /// number of values that were never observed.
35    Lapped {
36        /// Number of values skipped over by the lap-recovery jump.
37        skipped: usize,
38    },
39}
40
41/// A reading handle of an SPMC broadcast queue.
42///
43/// Each consumer has its own private read cursor: every consumer observes
44/// every value the producer publishes, independently, unless it falls behind
45/// by more than `CAPACITY` slots.
46///
47/// `Consumer` is [`Send`] but not [`Sync`]: at most one thread may call
48/// [`try_read`](Self::try_read) on a given consumer at a time. To get more
49/// consumers, clone this one. Each clone inherits the parent's current read
50/// cursor at the time of the clone, so a clone made after the parent has
51/// read `N` values will not re-observe those `N` values.
52pub struct Consumer<T, const CAPACITY: usize, A>
53where
54    T: bytemuck::AnyBitPattern,
55    A: Allocator,
56{
57    state: ConsumerState,
58    inner: Arc<Queue<T, CAPACITY, A>>,
59    _not_sync: PhantomData<*const ()>,
60}
61
62impl<T, const CAPACITY: usize, A> std::fmt::Debug for Consumer<T, CAPACITY, A>
63where
64    T: bytemuck::AnyBitPattern,
65    A: Allocator,
66{
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("Consumer")
69            .field("read_cursor", &self.state.read_cursor)
70            .field("cached_write_cursor", &self.state.cached_write_cursor)
71            .finish_non_exhaustive()
72    }
73}
74
75// SAFETY: It is Send on its own but we need to forbid the Sync.
76unsafe impl<T, const CAPACITY: usize, A> Send for Consumer<T, CAPACITY, A>
77where
78    A: Allocator,
79    T: bytemuck::AnyBitPattern + Send,
80    A::Allocation<Slot<T>>: Send,
81{
82}
83
84// An ordinary `#[derive(Clone)]` adds additional requirements such as `A: Clone` which is
85// actually irrelevant as we only use [`Allocator::Allocation<T>`].
86impl<T, const CAPACITY: usize, A> Clone for Consumer<T, CAPACITY, A>
87where
88    T: bytemuck::AnyBitPattern,
89    A: Allocator,
90{
91    fn clone(&self) -> Self {
92        Self {
93            state: self.state.clone(),
94            inner: self.inner.clone(),
95            _not_sync: self._not_sync.clone(),
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::mem::test_util::NeverAllocator;
104
105    static_assertions::assert_impl_all!(Consumer<u32, 2, NeverAllocator>: Send, Clone);
106    static_assertions::assert_not_impl_any!(Consumer<u32, 2, NeverAllocator>: Sync, Copy);
107}
108
109impl<T, const CAPACITY: usize, A> Consumer<T, CAPACITY, A>
110where
111    T: bytemuck::AnyBitPattern,
112    A: Allocator,
113{
114    pub(super) fn new(queue: Arc<Queue<T, CAPACITY, A>>) -> Self {
115        Self {
116            state: ConsumerStateInner {
117                read_cursor: 0,
118                cached_write_cursor: 0,
119            }
120            .into(),
121            inner: queue,
122            _not_sync: PhantomData,
123        }
124    }
125
126    /// Attempts to read the next value.
127    ///
128    /// Wait-free in the common case (value available, or queue empty);
129    /// lock-free when the producer is actively writing the target slot (the
130    /// consumer spin-loops until the producer publishes).
131    ///
132    /// # Protocol
133    ///
134    /// Each slot carries an even/odd seq number: even = stable, odd = mid
135    /// write. The consumer:
136    ///
137    /// 1. Loads the slot's seq. If odd, spin until it goes even.
138    /// 2. If the even seq is not the one expected for the current read
139    ///    cursor, the producer has lapped us; reload the write cursor, jump
140    ///    to the most recent slot, and return [`ReadResult::Lapped`].
141    /// 3. Otherwise read the slot's data, then re-load the seq. If the seq
142    ///    is unchanged the read is committed and a [`ReadResult::Value`] is
143    ///    returned; if the seq has moved, the read was torn and the loop
144    ///    retries.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use low_latency_data_structures::spmc::{self, ReadResult, new};
150    /// use low_latency_data_structures::mem::global::GlobalAllocator;
151    ///
152    /// let (producer, mut consumer) = new::<u64, 16, GlobalAllocator>(
153    ///     spmc::Options::global_mlocked(),
154    /// );
155    /// assert_eq!(consumer.try_read(), ReadResult::Empty);
156    /// producer.publish(7);
157    /// assert_eq!(consumer.try_read(), ReadResult::Value(7));
158    /// ```
159    #[inline]
160    pub fn try_read(&mut self) -> ReadResult<T> {
161        if self.state.read_cursor == self.state.cached_write_cursor {
162            // It still may not be empty
163            if self.is_still_empty() {
164                return ReadResult::Empty;
165            }
166        }
167        let slot = self.slot(self.state.read_cursor);
168        // That's the expected seq_no after producer has written the item
169        let expected_seq = self.state.read_cursor.wrapping_mul(2).wrapping_add(2);
170        loop {
171            let seq1 = slot.seq.load(Ordering::Acquire);
172            // If producer currently writes it, we wait (by the way, it means that we were overlapped)
173            if seq1 & 1 == 1 {
174                std::hint::spin_loop();
175                continue;
176            }
177            if seq1 != expected_seq {
178                // We were overlapped. Reload cached_write_cursor before jumping:
179                // under sustained producer overflow the cached value is itself
180                // stale (cached_write_cursor - 1 may have been lapped past
181                // again), and without the reload the consumer gets pinned in
182                // a Lapped -> Lapped cycle with skipped=0 forever.
183                self.state.cached_write_cursor = self
184                    .inner
185                    .producer_state
186                    .write_cursor
187                    .load(Ordering::Acquire);
188                let new_r_cursor = self.state.cached_write_cursor.wrapping_sub(1);
189                let skipped = new_r_cursor.wrapping_sub(self.state.read_cursor);
190                self.state.read_cursor = new_r_cursor;
191                return ReadResult::Lapped { skipped };
192            }
193            // SAFETY: T: AnyBitPattern, so even a torn read materialises a valid T;
194            // the seq2 check below rejects the value if the read raced a producer write.
195            let attempted_read = unsafe { slot.data.get().read_volatile() };
196            // ASM: prevent the seq1 load from moving below the read
197            fence(Ordering::Acquire);
198            let seq2 = slot.seq.load(Ordering::Relaxed);
199            if seq1 == seq2 {
200                self.state.read_cursor = self.state.read_cursor.wrapping_add(1);
201                return ReadResult::Value(attempted_read);
202            }
203            // Torn-read, retry
204        }
205    }
206
207    #[cold]
208    fn is_still_empty(&mut self) -> bool {
209        self.state.cached_write_cursor = self
210            .inner
211            .producer_state
212            .write_cursor
213            .load(Ordering::Acquire);
214        self.state.read_cursor == self.state.cached_write_cursor
215    }
216
217    /// Wraps the given `i` around `CAPACITY - 1`.
218    ///
219    /// # Safety
220    ///
221    /// - The masked index is always in `[0, CAPACITY)`, so the derived
222    ///   pointer stays inside the allocation.
223    /// - The allocation is aligned and dereferenceable per the `Allocator`
224    ///   contract.
225    /// - Every slot is initialized in [`new`](super::new) before the
226    ///   `Queue` is wrapped in `Arc`, so `assume_init_ref` is sound.
227    /// - Returning `&Slot<T>` while another thread mutates through
228    ///   `slot.data` / `slot.seq` is legal because the mutable state sits
229    ///   behind `UnsafeCell` and atomics.
230    #[inline(always)]
231    fn slot(&self, i: usize) -> &Slot<T> {
232        unsafe {
233            self.inner
234                .slots
235                .ptr()
236                .wrapping_add(i & (CAPACITY - 1))
237                .as_ref_unchecked()
238                .get()
239                .as_ref_unchecked()
240                .assume_init_ref()
241        }
242    }
243}