Skip to main content

agave_wake_channel/
lib.rs

1//! Futex-based wakeups for [`crossbeam_channel`] channels.
2//!
3//! When throughput is high and perf matters the most, this code is basically zero cost. When
4//! throughput is low, the futex based protocol described below kicks in, which is much more
5//! performant than crossbeam's `SyncWaker` (which, among other things, uses a mutex contended at
6//! every wake up by all senders and receivers).
7//!
8//! # Synchronization protocol
9//!
10//! Receivers sleep waiting for new messages when a channel is empty. After the underlying channel's
11//! `try_recv()` returns `Empty`, a receiver registers a waiter and calls `try_recv()` again before
12//! sleeping. If the channel is not empty anymore, the receiver returns the message immediately. If
13//! the channel is still empty, the receiver sleeps until a sender wakes it up.
14//!
15//! When a sender sends a message, it enqueues it into the channel and calls `wake_one()`. If no
16//! receivers are sleeping, `wake_one()` _does nothing_. If any receivers are sleeping, one is woken
17//! up so it can process the message.
18//!
19//! The fences in `register_waiter()` and `wake_*()` allow the "`wake_one()` does nothing"
20//! optimization, guaranteeing that either a receiver's second `try_recv()` observes a message, or
21//! the sender observes the registered waiter and so can wake it.
22
23#![cfg(feature = "agave-unstable-api")]
24
25#[cfg(feature = "shuttle-test")]
26use shuttle::sync::atomic::AtomicUsize;
27#[cfg(not(feature = "shuttle-test"))]
28use std::sync::atomic::AtomicUsize;
29use {
30    crossbeam_channel::{RecvError, SendError, TryRecvError},
31    crossbeam_utils::Backoff,
32    std::{
33        mem,
34        sync::{
35            Arc,
36            atomic::{AtomicU32, Ordering, fence},
37        },
38    },
39};
40
41/// Helper to coordinate sleeping and waking between senders and receivers.
42///
43/// See the crate docs for the synchronization protocol.
44#[derive(Default)]
45struct WakeEvent {
46    // waiters wait until the cookie changes
47    cookie: AtomicU32,
48    // number of waiters currently waiting on the cookie
49    waiters: AtomicUsize,
50}
51
52impl WakeEvent {
53    /// Returns a waiter that can be used to wait for this event to be signaled.
54    fn register_waiter(&self) -> WakeWaiter<'_> {
55        let cookie = self.cookie.load(Ordering::Relaxed);
56        self.waiters.fetch_add(1, Ordering::Relaxed);
57        // see Receiver::recv() on why this is needed
58        fence(Ordering::SeqCst);
59        WakeWaiter {
60            event: self,
61            cookie,
62        }
63    }
64
65    fn wake_one(&self) {
66        // see Receiver::recv() on why this is needed
67        fence(Ordering::SeqCst);
68        if self.waiters.load(Ordering::Relaxed) != 0 {
69            self.cookie.fetch_add(1, Ordering::Relaxed);
70            atomic_wait::wake_one(&self.cookie);
71        }
72    }
73
74    fn wake_all(&self) {
75        // see Receiver::recv() on why this is needed
76        fence(Ordering::SeqCst);
77        if self.waiters.load(Ordering::Relaxed) != 0 {
78            self.cookie.fetch_add(1, Ordering::Relaxed);
79            atomic_wait::wake_all(&self.cookie);
80        }
81    }
82}
83
84/// A waiter guard that can be used to wait for a wake event to be signaled.
85///
86/// When dropped, it automatically decrements the number of waiters on the event.
87struct WakeWaiter<'a> {
88    event: &'a WakeEvent,
89    cookie: u32,
90}
91
92impl WakeWaiter<'_> {
93    fn wait(self) {
94        if self.event.cookie.load(Ordering::Relaxed) == self.cookie {
95            atomic_wait::wait(&self.event.cookie, self.cookie);
96        }
97    }
98}
99
100impl Drop for WakeWaiter<'_> {
101    fn drop(&mut self) {
102        self.event.waiters.fetch_sub(1, Ordering::Relaxed);
103    }
104}
105
106struct Shared {
107    wake_event: WakeEvent,
108    // Used to keep track of the number of senders. Each sender drops its underlying channel handle
109    // before decrementing this count, so the last sender to decrement it can wake all waiters with
110    // the channel already disconnected.
111    num_senders: AtomicUsize,
112}
113
114/// Wrapper around [`crossbeam_channel::Sender`] that avoids contention by using a futex to wake
115/// sleeping receivers when a message is sent.
116pub struct Sender<T> {
117    inner: crossbeam_channel::Sender<T>,
118    shared: Arc<Shared>,
119}
120
121impl<T> Sender<T> {
122    pub fn send(&self, value: T) -> Result<(), SendError<T>> {
123        self.inner.send(value)?;
124        self.shared.wake_event.wake_one();
125        Ok(())
126    }
127}
128
129impl<T> Clone for Sender<T> {
130    fn clone(&self) -> Self {
131        self.shared.num_senders.fetch_add(1, Ordering::Relaxed);
132        Self {
133            inner: self.inner.clone(),
134            shared: Arc::clone(&self.shared),
135        }
136    }
137}
138
139impl<T> Drop for Sender<T> {
140    fn drop(&mut self) {
141        let (replacement, _) = crossbeam_channel::bounded(0);
142        drop(mem::replace(&mut self.inner, replacement));
143        if self.shared.num_senders.fetch_sub(1, Ordering::AcqRel) == 1 {
144            self.shared.wake_event.wake_all();
145        }
146    }
147}
148
149/// Wrapper around [`crossbeam_channel::Receiver`] that avoids contention by using a futex to sleep
150/// waiting for messages when the channel is empty.
151pub struct Receiver<T> {
152    inner: crossbeam_channel::Receiver<T>,
153    shared: Arc<Shared>,
154}
155
156impl<T> Receiver<T> {
157    pub fn recv(&self) -> Result<T, RecvError> {
158        loop {
159            let backoff = Backoff::new();
160            loop {
161                match self.inner.try_recv() {
162                    Ok(value) => return Ok(value),
163                    Err(TryRecvError::Disconnected) => return Err(RecvError),
164                    Err(TryRecvError::Empty) if backoff.is_completed() => break,
165                    Err(TryRecvError::Empty) => backoff.snooze(),
166                }
167            }
168
169            // There's a potential race in the window between getting `Empty` above, and registering
170            // the waiter below. A sender might queue a message in the meantime, `wake_one()` might
171            // observe `wake_event.waiters == 0` and skip the wake syscall (optimization to avoid
172            // one syscall per message when the channel has large bursts when it's not empty).
173            //
174            // So below we check again, this time _after_ calling `register_waiter()` and so with
175            // `wake_event.waiters` guaranteed to be non zero. The paired fences (see
176            // `register_waiter()` and `wake_*()`) guarantee that either this check observes a
177            // message into the channel (or `Disconnected`), or the sender sees us in `wait()` and
178            // wakes us.
179            let waiter = self.shared.wake_event.register_waiter();
180            match self.inner.try_recv() {
181                Ok(value) => return Ok(value),
182                Err(TryRecvError::Disconnected) => return Err(RecvError),
183                Err(TryRecvError::Empty) => waiter.wait(),
184            }
185        }
186    }
187}
188
189impl<T> Clone for Receiver<T> {
190    fn clone(&self) -> Self {
191        Self {
192            inner: self.inner.clone(),
193            shared: Arc::clone(&self.shared),
194        }
195    }
196}
197
198pub fn bounded<T>(capacity: usize) -> (Sender<T>, Receiver<T>) {
199    assert_ne!(capacity, 0, "channel capacity must be nonzero");
200    let (sender, receiver) = crossbeam_channel::bounded(capacity);
201    let shared = Arc::new(Shared {
202        wake_event: WakeEvent::default(),
203        num_senders: AtomicUsize::new(1),
204    });
205    (
206        Sender {
207            inner: sender,
208            shared: Arc::clone(&shared),
209        },
210        Receiver {
211            inner: receiver,
212            shared,
213        },
214    )
215}
216
217#[cfg(all(test, not(feature = "shuttle-test")))]
218mod tests {
219    use {
220        super::*,
221        std::{sync::Barrier, thread},
222    };
223
224    #[test]
225    fn test_wake_before_wait() {
226        let event = WakeEvent::default();
227        let waiter = event.register_waiter();
228        event.wake_one();
229        waiter.wait();
230    }
231
232    #[test]
233    fn test_wake_receivers_and_disconnect() {
234        const NUM_RECEIVERS: usize = 4;
235
236        let (sender, receiver) = bounded(NUM_RECEIVERS);
237        let sender1 = sender.clone();
238        drop(sender);
239        let barrier = Arc::new(Barrier::new(NUM_RECEIVERS + 1));
240        let handles = (0..NUM_RECEIVERS)
241            .map(|_| {
242                let receiver = receiver.clone();
243                let barrier = Arc::clone(&barrier);
244                thread::spawn(move || {
245                    receiver.recv().unwrap();
246                    barrier.wait();
247                    assert!(receiver.recv().is_err());
248                })
249            })
250            .collect::<Vec<_>>();
251
252        while receiver.shared.wake_event.waiters.load(Ordering::Relaxed) != NUM_RECEIVERS {
253            thread::yield_now();
254        }
255        for _ in 0..NUM_RECEIVERS {
256            sender1.send(()).unwrap();
257        }
258        barrier.wait();
259        drop(sender1);
260
261        for handle in handles {
262            handle.join().unwrap();
263        }
264    }
265}
266
267#[cfg(all(test, feature = "shuttle-test"))]
268mod shuttle_tests {
269    use {super::*, shuttle::thread};
270
271    #[test]
272    fn test_disconnect_is_visible_before_wake() {
273        shuttle::check_dfs(
274            || {
275                let (sender, receiver) = bounded::<()>(1);
276                let sender1 = sender.clone();
277                let observer_receiver = receiver.clone();
278                let _waiter = receiver.shared.wake_event.register_waiter();
279
280                let sender_drop = thread::spawn(move || drop(sender));
281                let sender1_drop = thread::spawn(move || drop(sender1));
282                let observer = thread::spawn(move || {
283                    // A changed cookie means wake_all() has run. The underlying channel must have
284                    // been disconnected before the wake became visible.
285                    if observer_receiver
286                        .shared
287                        .wake_event
288                        .cookie
289                        .load(Ordering::Relaxed)
290                        != 0
291                    {
292                        assert_eq!(
293                            observer_receiver.inner.try_recv(),
294                            Err(TryRecvError::Disconnected),
295                        );
296                    }
297                });
298
299                sender_drop.join().unwrap();
300                sender1_drop.join().unwrap();
301                observer.join().unwrap();
302                assert_ne!(receiver.shared.wake_event.cookie.load(Ordering::Relaxed), 0);
303                assert_eq!(receiver.inner.try_recv(), Err(TryRecvError::Disconnected));
304            },
305            None,
306        );
307    }
308}