Browse Source

feat(mpsc): add support for statically-allocated MPSC channels (#23)

This branch adds new `StaticChannel` types that can be used to construct
statically allocated MPSC channel variants. The static channels can be
used without requiring *any* heap allocations. This is intended
primarily for embedded systems and bare metal programming where
allocators may not be available or heap memory is constrained.

Closes #17

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Eliza Weisman 3 years ago
parent
commit
5b17c184b0

+ 2 - 2
Cargo.toml

@@ -12,13 +12,13 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [features]
-std = ["alloc"]
+std = ["alloc", "parking_lot"]
 alloc = []
 default = ["std"]
 
 [dependencies]
 pin-project = "1"
-parking_lot = { version = "0.11", optional = true }
+parking_lot = { version = "0.11", optional = true, default-features = false }
 
 [dev-dependencies]
 tokio = { version = "1.14.0", features = ["rt", "rt-multi-thread", "macros", "sync"] }

+ 4 - 4
bench/benches/async_mpsc.rs

@@ -25,8 +25,8 @@ aaaaaaaaaaaaaa";
             &senders,
             |b, &senders| {
                 b.to_async(rt()).iter(|| async {
-                    use thingbuf::{mpsc, ThingBuf};
-                    let (tx, rx) = mpsc::channel(ThingBuf::<String>::new(CAPACITY));
+                    use thingbuf::mpsc;
+                    let (tx, rx) = mpsc::channel::<String>(CAPACITY);
                     for _ in 0..senders {
                         let tx = tx.clone();
                         task::spawn(async move {
@@ -149,8 +149,8 @@ fn bench_mpsc_integer(c: &mut Criterion) {
             &senders,
             |b, &senders| {
                 b.to_async(rt()).iter(|| async {
-                    use thingbuf::{mpsc, ThingBuf};
-                    let (tx, rx) = mpsc::channel(ThingBuf::new(CAPACITY));
+                    use thingbuf::mpsc;
+                    let (tx, rx) = mpsc::channel::<i32>(CAPACITY);
                     for i in 0..senders {
                         let tx = tx.clone();
                         task::spawn(async move {

+ 7 - 12
bench/benches/async_spsc.rs

@@ -22,11 +22,9 @@ aaaaaaaaaaaaaa";
         group.bench_with_input(BenchmarkId::new("ThingBuf", size), &size, |b, &i| {
             let rt = runtime::Builder::new_current_thread().build().unwrap();
             b.to_async(rt).iter(|| async {
-                use thingbuf::{
-                    mpsc::{self, TrySendError},
-                    ThingBuf,
-                };
-                let (tx, rx) = mpsc::channel(ThingBuf::<String>::new(100));
+                use thingbuf::mpsc::{self, TrySendError};
+
+                let (tx, rx) = mpsc::channel::<String>(100);
                 task::spawn(async move {
                     loop {
                         match tx.try_send_ref() {
@@ -158,8 +156,8 @@ aaaaaaaaaaaaaa";
         group.bench_with_input(BenchmarkId::new("ThingBuf", size), &size, |b, &i| {
             let rt = runtime::Builder::new_current_thread().build().unwrap();
             b.to_async(rt).iter(|| async {
-                use thingbuf::{mpsc, ThingBuf};
-                let (tx, rx) = mpsc::channel(ThingBuf::<String>::new(100));
+                use thingbuf::mpsc;
+                let (tx, rx) = mpsc::channel::<String>(100);
                 task::spawn(async move {
                     while let Ok(mut slot) = tx.send_ref().await {
                         slot.clear();
@@ -267,11 +265,8 @@ fn bench_spsc_try_send_integer(c: &mut Criterion) {
         group.bench_with_input(BenchmarkId::new("ThingBuf", size), &size, |b, &i| {
             let rt = runtime::Builder::new_current_thread().build().unwrap();
             b.to_async(rt).iter(|| async {
-                use thingbuf::{
-                    mpsc::{self, TrySendError},
-                    ThingBuf,
-                };
-                let (tx, rx) = mpsc::channel(ThingBuf::new(100));
+                use thingbuf::mpsc::{self, TrySendError};
+                let (tx, rx) = mpsc::channel(100);
                 task::spawn(async move {
                     let mut i = 0;
                     loop {

+ 4 - 7
bench/benches/sync_spsc.rs

@@ -20,11 +20,8 @@ aaaaaaaaaaaaaa";
         group.throughput(Throughput::Elements(size));
         group.bench_with_input(BenchmarkId::new("ThingBuf", size), &size, |b, &i| {
             b.iter(|| {
-                use thingbuf::{
-                    mpsc::{sync, TrySendError},
-                    ThingBuf,
-                };
-                let (tx, rx) = sync::channel(ThingBuf::<String>::new(100));
+                use thingbuf::mpsc::{sync, TrySendError};
+                let (tx, rx) = sync::channel::<String>(100);
                 let producer = thread::spawn(move || loop {
                     match tx.try_send_ref() {
                         Ok(mut slot) => {
@@ -108,8 +105,8 @@ aaaaaaaaaaaaaa";
         group.throughput(Throughput::Elements(size));
         group.bench_with_input(BenchmarkId::new("ThingBuf", size), &size, |b, &i| {
             b.iter(|| {
-                use thingbuf::{mpsc::sync, ThingBuf};
-                let (tx, rx) = sync::channel(ThingBuf::<String>::new(100));
+                use thingbuf::mpsc::sync;
+                let (tx, rx) = sync::channel::<String>(100);
                 let producer = thread::spawn(move || {
                     while let Ok(mut slot) = tx.send_ref() {
                         slot.clear();

+ 2 - 2
src/lib.rs

@@ -19,10 +19,10 @@ feature! {
 
     mod stringbuf;
     pub use stringbuf::{StaticStringBuf, StringBuf};
-
-    pub mod mpsc;
 }
 
+pub mod mpsc;
+
 mod static_thingbuf;
 pub use self::static_thingbuf::StaticThingBuf;
 

+ 57 - 24
src/mpsc.rs

@@ -13,11 +13,9 @@
 use crate::{
     loom::{atomic::AtomicUsize, hint},
     wait::{Notify, WaitCell, WaitQueue, WaitResult},
-    Ref, ThingBuf,
+    Core, Ref, Slot,
 };
-use core::fmt;
-use core::pin::Pin;
-use core::task::Poll;
+use core::{fmt, ops::Index, task::Poll};
 
 #[derive(Debug)]
 #[non_exhaustive]
@@ -30,8 +28,8 @@ pub enum TrySendError<T = ()> {
 pub struct Closed<T = ()>(T);
 
 #[derive(Debug)]
-struct Inner<T, N: Notify> {
-    thingbuf: ThingBuf<T>,
+struct ChannelCore<N> {
+    core: Core,
     rx_wait: WaitCell<N>,
     tx_count: AtomicUsize,
     tx_wait: WaitQueue<N>,
@@ -72,18 +70,34 @@ impl TrySendError {
 }
 
 // ==== impl Inner ====
-impl<T, N: Notify + Unpin> Inner<T, N> {
-    fn new(thingbuf: ThingBuf<T>) -> Self {
+impl<N> ChannelCore<N> {
+    #[cfg(not(loom))]
+    const fn new(capacity: usize) -> Self {
         Self {
-            thingbuf,
+            core: Core::new(capacity),
             rx_wait: WaitCell::new(),
             tx_count: AtomicUsize::new(1),
             tx_wait: WaitQueue::new(),
         }
     }
 
+    #[cfg(loom)]
+    fn new(capacity: usize) -> Self {
+        Self {
+            core: Core::new(capacity),
+            rx_wait: WaitCell::new(),
+            tx_count: AtomicUsize::new(1),
+            tx_wait: WaitQueue::new(),
+        }
+    }
+}
+
+impl<N> ChannelCore<N>
+where
+    N: Notify + Unpin,
+{
     fn close_rx(&self) {
-        if self.thingbuf.core.close() {
+        if self.core.close() {
             crate::loom::hint::spin_loop();
             test_println!("draining_queue");
             self.tx_wait.close();
@@ -91,19 +105,28 @@ impl<T, N: Notify + Unpin> Inner<T, N> {
     }
 }
 
-impl<T: Default, N: Notify + Unpin> Inner<T, N> {
-    fn try_send_ref(&self) -> Result<SendRefInner<'_, T, N>, TrySendError> {
-        self.thingbuf
-            .core
-            .push_ref(self.thingbuf.slots.as_ref())
-            .map(|slot| SendRefInner {
-                _notify: NotifyRx(&self.rx_wait),
-                slot,
-            })
+impl<N> ChannelCore<N>
+where
+    N: Notify + Unpin,
+{
+    fn try_send_ref<'a, T>(
+        &'a self,
+        slots: &'a [Slot<T>],
+    ) -> Result<SendRefInner<'a, T, N>, TrySendError>
+    where
+        T: Default,
+    {
+        self.core.push_ref(slots).map(|slot| SendRefInner {
+            _notify: NotifyRx(&self.rx_wait),
+            slot,
+        })
     }
 
-    fn try_send(&self, val: T) -> Result<(), TrySendError<T>> {
-        match self.try_send_ref() {
+    fn try_send<T>(&self, slots: &[Slot<T>], val: T) -> Result<(), TrySendError<T>>
+    where
+        T: Default,
+    {
+        match self.try_send_ref(slots) {
             Ok(mut slot) => {
                 slot.with_mut(|slot| *slot = val);
                 Ok(())
@@ -117,11 +140,19 @@ impl<T: Default, N: Notify + Unpin> Inner<T, N> {
     /// The loop itself has to be written in the actual `send` method's
     /// implementation, rather than on `inner`, because it might be async and
     /// may yield, or might park the thread.
-    fn poll_recv_ref(&self, mk_waiter: impl Fn() -> N) -> Poll<Option<Ref<'_, T>>> {
+    fn poll_recv_ref<'a, T, S>(
+        &'a self,
+        slots: &'a S,
+        mk_waiter: impl Fn() -> N,
+    ) -> Poll<Option<Ref<'a, T>>>
+    where
+        S: Index<usize, Output = Slot<T>> + ?Sized,
+        T: Default,
+    {
         macro_rules! try_poll_recv {
             () => {
                 // If we got a value, return it!
-                match self.thingbuf.core.pop_ref(self.thingbuf.slots.as_ref()) {
+                match self.core.pop_ref(slots) {
                     Ok(slot) => return Poll::Ready(Some(slot)),
                     Err(TrySendError::Closed(_)) => return Poll::Ready(None),
                     _ => {}
@@ -151,7 +182,7 @@ impl<T: Default, N: Notify + Unpin> Inner<T, N> {
                     // the channel is closed (all the receivers are dropped).
                     // however, there may be messages left in the queue. try
                     // popping from the queue until it's empty.
-                    return Poll::Ready(self.thingbuf.pop_ref());
+                    return Poll::Ready(self.core.pop_ref(slots).ok());
                 }
                 WaitResult::Notified => {
                     // we were notified while we were trying to register the
@@ -163,6 +194,8 @@ impl<T: Default, N: Notify + Unpin> Inner<T, N> {
     }
 }
 
+// === impl SendRefInner ===
+
 impl<T, N: Notify> core::ops::Deref for SendRefInner<'_, T, N> {
     type Target = T;
 

+ 453 - 132
src/mpsc/async_impl.rs

@@ -1,11 +1,8 @@
 use super::*;
 use crate::{
-    loom::{
-        atomic::{self, Ordering},
-        sync::Arc,
-    },
+    loom::atomic::{self, AtomicBool, Ordering},
     wait::queue,
-    Ref, ThingBuf,
+    Ref,
 };
 use core::{
     fmt,
@@ -14,24 +11,88 @@ use core::{
     task::{Context, Poll, Waker},
 };
 
-/// Returns a new synchronous multi-producer, single consumer channel.
-pub fn channel<T>(thingbuf: ThingBuf<T>) -> (Sender<T>, Receiver<T>) {
-    let inner = Arc::new(Inner::new(thingbuf));
-    let tx = Sender {
-        inner: inner.clone(),
-    };
-    let rx = Receiver { inner };
-    (tx, rx)
+feature! {
+    #![feature = "alloc"]
+
+    use crate::loom::sync::Arc;
+
+    /// Returns a new synchronous multi-producer, single consumer channel.
+    pub fn channel<T: Default>(capacity: usize) -> (Sender<T>, Receiver<T>) {
+        assert!(capacity > 0);
+        let slots = (0..capacity).map(|_| Slot::empty()).collect();
+        let inner = Arc::new(Inner {
+            core: ChannelCore::new(capacity),
+            slots,
+        });
+        let tx = Sender {
+            inner: inner.clone(),
+        };
+        let rx = Receiver { inner };
+        (tx, rx)
+    }
+
+    #[derive(Debug)]
+
+    pub struct Receiver<T> {
+        inner: Arc<Inner<T>>,
+    }
+
+    #[derive(Debug)]
+    pub struct Sender<T> {
+        inner: Arc<Inner<T>>,
+    }
+
+    struct Inner<T> {
+        core: super::ChannelCore<Waker>,
+        slots: Box<[Slot<T>]>,
+    }
+}
+
+/// A statically-allocated, asynchronous bounded MPSC channel.
+///
+/// A statically-allocated channel allows using a MPSC channel without
+/// requiring _any_ heap allocations, and can be used in environments that
+/// don't support `liballoc`.
+///
+/// In order to use a statically-allocated channel, a `StaticChannel` must
+/// be constructed in a `static` initializer. This reserves storage for the
+/// channel's message queue at compile-time. Then, at runtime, the channel
+/// is [`split`] into a [`StaticSender`]/[`StaticReceiver`] pair in order to
+/// be used.
+///
+/// # Examples
+///
+/// ```
+/// use thingbuf::mpsc::StaticChannel;
+///
+/// // Construct a statically-allocated channel of `usize`s with a capacity
+/// // of 16 messages.
+/// static MY_CHANNEL: StaticChannel<usize, 16> = StaticChannel::new();
+///
+/// fn main() {
+///     // Split the `StaticChannel` into a sender-receiver pair.
+///     let (tx, rx) = MY_CHANNEL.split();
+///
+///     // Now, `tx` and `rx` can be used just like any other async MPSC
+///     // channel...
+/// # drop(tx); drop(rx);
+/// }
+/// ```
+/// [`split`]: StaticChannel::split
+pub struct StaticChannel<T, const CAPACITY: usize> {
+    core: ChannelCore<Waker>,
+    slots: [Slot<T>; CAPACITY],
+    is_split: AtomicBool,
 }
 
-#[derive(Debug)]
-pub struct Sender<T> {
-    inner: Arc<Inner<T, Waker>>,
+pub struct StaticSender<T: 'static> {
+    core: &'static ChannelCore<Waker>,
+    slots: &'static [Slot<T>],
 }
 
-#[derive(Debug)]
-pub struct Receiver<T> {
-    inner: Arc<Inner<T, Waker>>,
+pub struct StaticReceiver<T: 'static> {
+    core: &'static ChannelCore<Waker>,
+    slots: &'static [Slot<T>],
 }
 
 impl_send_ref! {
@@ -47,7 +108,8 @@ impl_recv_ref! {
 /// This type is returned by [`Receiver::recv_ref`].
 #[must_use = "futures do nothing unless you `.await` or poll them"]
 pub struct RecvRefFuture<'a, T> {
-    rx: &'a Receiver<T>,
+    core: &'a ChannelCore<Waker>,
+    slots: &'a [Slot<T>],
 }
 
 /// A [`Future`] that tries to receive a value from a [`Receiver`].
@@ -57,114 +119,252 @@ pub struct RecvRefFuture<'a, T> {
 /// This is equivalent to the [`RecvRefFuture`] future, but the value is moved out of
 /// the [`ThingBuf`] after it is received. This means that allocations are not
 /// reused.
+///
+/// [`ThingBuf`]: crate::ThingBuf
 #[must_use = "futures do nothing unless you `.await` or poll them"]
 pub struct RecvFuture<'a, T> {
-    rx: &'a Receiver<T>,
+    core: &'a ChannelCore<Waker>,
+    slots: &'a [Slot<T>],
+}
+
+#[pin_project::pin_project(PinnedDrop)]
+struct SendRefFuture<'sender, T> {
+    core: &'sender ChannelCore<Waker>,
+    slots: &'sender [Slot<T>],
+    state: State,
+    #[pin]
+    waiter: queue::Waiter<Waker>,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+enum State {
+    Start,
+    Waiting,
+    Done,
+}
+
+// === impl StaticChannel ===
+
+#[cfg(not(all(loom, test)))]
+impl<T, const CAPACITY: usize> StaticChannel<T, CAPACITY> {
+    const SLOT: Slot<T> = Slot::empty();
+
+    /// Constructs a new statically-allocated, asynchronous bounded MPSC channel.
+    ///
+    /// A statically-allocated channel allows using a MPSC channel without
+    /// requiring _any_ heap allocations, and can be used in environments that
+    /// don't support `liballoc`.
+    ///
+    /// In order to use a statically-allocated channel, a `StaticChannel` must
+    /// be constructed in a `static` initializer. This reserves storage for the
+    /// channel's message queue at compile-time. Then, at runtime, the channel
+    /// is [`split`] into a [`StaticSender`]/[`StaticReceiver`] pair in order to
+    /// be used.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use thingbuf::mpsc::StaticChannel;
+    ///
+    /// // Construct a statically-allocated channel of `usize`s with a capacity
+    /// // of 16 messages.
+    /// static MY_CHANNEL: StaticChannel<usize, 16> = StaticChannel::new();
+    ///
+    /// fn main() {
+    ///     // Split the `StaticChannel` into a sender-receiver pair.
+    ///     let (tx, rx) = MY_CHANNEL.split();
+    ///
+    ///     // Now, `tx` and `rx` can be used just like any other async MPSC
+    ///     // channel...
+    /// # drop(tx); drop(rx);
+    /// }
+    /// ```
+    /// [`split`]: StaticChannel::split
+    pub const fn new() -> Self {
+        Self {
+            core: ChannelCore::new(CAPACITY),
+            slots: [Self::SLOT; CAPACITY],
+            is_split: AtomicBool::new(false),
+        }
+    }
+
+    /// Split a [`StaticChannel`] into a [`StaticSender`]/[`StaticReceiver`]
+    /// pair.
+    ///
+    /// A static channel can only be split a single time. If
+    /// [`StaticChannel::split`] or [`StaticChannel::try_split`] have been
+    /// called previously, this method will panic. For a non-panicking version
+    /// of this method, see [`StaticChannel::try_split`].
+    ///
+    /// # Panics
+    ///
+    /// If the channel has already been split.
+    pub fn split(&'static self) -> (StaticSender<T>, StaticReceiver<T>) {
+        self.try_split().expect("channel already split")
+    }
+
+    /// Try to split a [`StaticChannel`] into a [`StaticSender`]/[`StaticReceiver`]
+    /// pair, returning `None` if it has already been split.
+    ///
+    /// A static channel can only be split a single time. If
+    /// [`StaticChannel::split`] or [`StaticChannel::try_split`] have been
+    /// called previously, this method returns `None`.
+    pub fn try_split(&'static self) -> Option<(StaticSender<T>, StaticReceiver<T>)> {
+        self.is_split
+            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
+            .ok()?;
+        let tx = StaticSender {
+            core: &self.core,
+            slots: &self.slots[..],
+        };
+        let rx = StaticReceiver {
+            core: &self.core,
+            slots: &self.slots[..],
+        };
+        Some((tx, rx))
+    }
 }
 
 // === impl Sender ===
 
+#[cfg(feature = "alloc")]
 impl<T: Default> Sender<T> {
     pub fn try_send_ref(&self) -> Result<SendRef<'_, T>, TrySendError> {
-        self.inner.try_send_ref().map(SendRef)
+        self.inner
+            .core
+            .try_send_ref(self.inner.slots.as_ref())
+            .map(SendRef)
     }
 
     pub fn try_send(&self, val: T) -> Result<(), TrySendError<T>> {
-        self.inner.try_send(val)
+        self.inner.core.try_send(self.inner.slots.as_ref(), val)
     }
 
     pub async fn send_ref(&self) -> Result<SendRef<'_, T>, Closed> {
-        #[pin_project::pin_project(PinnedDrop)]
-        struct SendRefFuture<'sender, T> {
-            tx: &'sender Sender<T>,
-            state: State,
-            #[pin]
-            waiter: queue::Waiter<Waker>,
+        SendRefFuture {
+            core: &self.inner.core,
+            slots: self.inner.slots.as_ref(),
+            state: State::Start,
+            waiter: queue::Waiter::new(),
+        }
+        .await
+    }
+
+    pub async fn send(&self, val: T) -> Result<(), Closed<T>> {
+        match self.send_ref().await {
+            Err(Closed(())) => Err(Closed(val)),
+            Ok(mut slot) => {
+                slot.with_mut(|slot| *slot = val);
+                Ok(())
+            }
         }
+    }
+}
 
-        #[derive(Debug, Copy, Clone, Eq, PartialEq)]
-        enum State {
-            Start,
-            Waiting,
-            Done,
+#[cfg(feature = "alloc")]
+impl<T> Clone for Sender<T> {
+    fn clone(&self) -> Self {
+        test_dbg!(self.inner.core.tx_count.fetch_add(1, Ordering::Relaxed));
+        Self {
+            inner: self.inner.clone(),
         }
+    }
+}
 
-        impl<'sender, T: Default + 'sender> Future for SendRefFuture<'sender, T> {
-            type Output = Result<SendRef<'sender, T>, Closed>;
-
-            fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
-                test_println!("SendRefFuture::poll({:p})", self);
-
-                loop {
-                    let this = self.as_mut().project();
-                    let node = this.waiter;
-                    match test_dbg!(*this.state) {
-                        State::Start => {
-                            match this.tx.try_send_ref() {
-                                Ok(slot) => return Poll::Ready(Ok(slot)),
-                                Err(TrySendError::Closed(_)) => {
-                                    return Poll::Ready(Err(Closed(())))
-                                }
-                                Err(_) => {}
-                            }
-
-                            let start_wait = this.tx.inner.tx_wait.start_wait(node, cx.waker());
-
-                            match test_dbg!(start_wait) {
-                                WaitResult::Closed => {
-                                    // the channel closed while we were registering the waiter!
-                                    *this.state = State::Done;
-                                    return Poll::Ready(Err(Closed(())));
-                                }
-                                WaitResult::Wait => {
-                                    // okay, we are now queued to wait.
-                                    // gotosleep!
-                                    *this.state = State::Waiting;
-                                    return Poll::Pending;
-                                }
-                                WaitResult::Notified => continue,
-                            }
-                        }
-                        State::Waiting => {
-                            let continue_wait =
-                                this.tx.inner.tx_wait.continue_wait(node, cx.waker());
-
-                            match test_dbg!(continue_wait) {
-                                WaitResult::Closed => {
-                                    *this.state = State::Done;
-                                    return Poll::Ready(Err(Closed(())));
-                                }
-                                WaitResult::Wait => return Poll::Pending,
-                                WaitResult::Notified => {
-                                    *this.state = State::Done;
-                                }
-                            }
-                        }
-                        State::Done => match this.tx.try_send_ref() {
-                            Ok(slot) => return Poll::Ready(Ok(slot)),
-                            Err(TrySendError::Closed(_)) => return Poll::Ready(Err(Closed(()))),
-                            Err(_) => {
-                                *this.state = State::Start;
-                            }
-                        },
-                    }
-                }
-            }
+#[cfg(feature = "alloc")]
+impl<T> Drop for Sender<T> {
+    fn drop(&mut self) {
+        if test_dbg!(self.inner.core.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
+            return;
         }
 
-        #[pin_project::pinned_drop]
-        impl<T> PinnedDrop for SendRefFuture<'_, T> {
-            fn drop(self: Pin<&mut Self>) {
-                test_println!("SendRefFuture::drop({:p})", self);
-                let this = self.project();
-                if test_dbg!(*this.state) == State::Waiting && test_dbg!(this.waiter.is_linked()) {
-                    this.waiter.remove(&this.tx.inner.tx_wait)
-                }
-            }
+        // if we are the last sender, synchronize
+        test_dbg!(atomic::fence(Ordering::SeqCst));
+        self.inner.core.core.close();
+        self.inner.core.rx_wait.close_tx();
+    }
+}
+
+// === impl Receiver ===
+
+#[cfg(feature = "alloc")]
+impl<T: Default> Receiver<T> {
+    pub fn recv_ref(&self) -> RecvRefFuture<'_, T> {
+        RecvRefFuture {
+            core: &self.inner.core,
+            slots: self.inner.slots.as_ref(),
+        }
+    }
+
+    pub fn recv(&self) -> RecvFuture<'_, T> {
+        RecvFuture {
+            core: &self.inner.core,
+            slots: self.inner.slots.as_ref(),
         }
+    }
 
+    /// # Returns
+    ///
+    ///  * `Poll::Pending` if no messages are available but the channel is not
+    ///    closed, or if a spurious failure happens.
+    ///  * `Poll::Ready(Some(Ref<T>))` if a message is available.
+    ///  * `Poll::Ready(None)` if the channel has been closed and all messages
+    ///    sent before it was closed have been received.
+    ///
+    /// When the method returns [`Poll::Pending`], the [`Waker`] in the provided
+    /// [`Context`] is scheduled to receive a wakeup when a message is sent on any
+    /// sender, or when the channel is closed.  Note that on multiple calls to
+    /// `poll_recv_ref`, only the [`Waker`] from the [`Context`] passed to the most
+    /// recent call is scheduled to receive a wakeup.
+    pub fn poll_recv_ref(&self, cx: &mut Context<'_>) -> Poll<Option<RecvRef<'_, T>>> {
+        poll_recv_ref(&self.inner.core, &self.inner.slots, cx)
+    }
+
+    /// # Returns
+    ///
+    ///  * `Poll::Pending` if no messages are available but the channel is not
+    ///    closed, or if a spurious failure happens.
+    ///  * `Poll::Ready(Some(message))` if a message is available.
+    ///  * `Poll::Ready(None)` if the channel has been closed and all messages
+    ///    sent before it was closed have been received.
+    ///
+    /// When the method returns [`Poll::Pending`], the [`Waker`] in the provided
+    /// [`Context`] is scheduled to receive a wakeup when a message is sent on any
+    /// sender, or when the channel is closed.  Note that on multiple calls to
+    /// `poll_recv`, only the [`Waker`] from the [`Context`] passed to the most
+    /// recent call is scheduled to receive a wakeup.
+    pub fn poll_recv(&self, cx: &mut Context<'_>) -> Poll<Option<T>> {
+        self.poll_recv_ref(cx)
+            .map(|opt| opt.map(|mut r| r.with_mut(core::mem::take)))
+    }
+
+    pub fn is_closed(&self) -> bool {
+        test_dbg!(self.inner.core.tx_count.load(Ordering::SeqCst)) <= 1
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl<T> Drop for Receiver<T> {
+    fn drop(&mut self) {
+        self.inner.core.close_rx();
+    }
+}
+
+// === impl StaticSender ===
+
+impl<T: Default> StaticSender<T> {
+    pub fn try_send_ref(&self) -> Result<SendRef<'_, T>, TrySendError> {
+        self.core.try_send_ref(self.slots).map(SendRef)
+    }
+
+    pub fn try_send(&self, val: T) -> Result<(), TrySendError<T>> {
+        self.core.try_send(self.slots, val)
+    }
+
+    pub async fn send_ref(&self) -> Result<SendRef<'_, T>, Closed> {
         SendRefFuture {
-            tx: self,
+            core: self.core,
+            slots: self.slots,
             state: State::Start,
             waiter: queue::Waiter::new(),
         }
@@ -182,37 +382,53 @@ impl<T: Default> Sender<T> {
     }
 }
 
-impl<T> Clone for Sender<T> {
+impl<T> Clone for StaticSender<T> {
     fn clone(&self) -> Self {
-        test_dbg!(self.inner.tx_count.fetch_add(1, Ordering::Relaxed));
+        test_dbg!(self.core.tx_count.fetch_add(1, Ordering::Relaxed));
         Self {
-            inner: self.inner.clone(),
+            core: self.core,
+            slots: self.slots,
         }
     }
 }
 
-impl<T> Drop for Sender<T> {
+impl<T> Drop for StaticSender<T> {
     fn drop(&mut self) {
-        if test_dbg!(self.inner.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
+        if test_dbg!(self.core.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
             return;
         }
 
         // if we are the last sender, synchronize
         test_dbg!(atomic::fence(Ordering::SeqCst));
-        self.inner.thingbuf.core.close();
-        self.inner.rx_wait.close_tx();
+        self.core.core.close();
+        self.core.rx_wait.close_tx();
     }
 }
 
-// === impl Receiver ===
+impl<T> fmt::Debug for StaticSender<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("StaticSender")
+            .field("core", &self.core)
+            .field("slots", &format_args!("&[..]"))
+            .finish()
+    }
+}
 
-impl<T: Default> Receiver<T> {
+// === impl StaticReceiver ===
+
+impl<T: Default> StaticReceiver<T> {
     pub fn recv_ref(&self) -> RecvRefFuture<'_, T> {
-        RecvRefFuture { rx: self }
+        RecvRefFuture {
+            core: self.core,
+            slots: self.slots,
+        }
     }
 
     pub fn recv(&self) -> RecvFuture<'_, T> {
-        RecvFuture { rx: self }
+        RecvFuture {
+            core: self.core,
+            slots: self.slots,
+        }
     }
 
     /// # Returns
@@ -229,12 +445,7 @@ impl<T: Default> Receiver<T> {
     /// `poll_recv_ref`, only the [`Waker`] from the [`Context`] passed to the most
     /// recent call is scheduled to receive a wakeup.
     pub fn poll_recv_ref(&self, cx: &mut Context<'_>) -> Poll<Option<RecvRef<'_, T>>> {
-        self.inner.poll_recv_ref(|| cx.waker().clone()).map(|some| {
-            some.map(|slot| RecvRef {
-                _notify: super::NotifyTx(&self.inner.tx_wait),
-                slot,
-            })
-        })
+        poll_recv_ref(self.core, self.slots, cx)
     }
 
     /// # Returns
@@ -256,23 +467,47 @@ impl<T: Default> Receiver<T> {
     }
 
     pub fn is_closed(&self) -> bool {
-        test_dbg!(self.inner.tx_count.load(Ordering::SeqCst)) <= 1
+        test_dbg!(self.core.tx_count.load(Ordering::SeqCst)) <= 1
     }
 }
 
-impl<T> Drop for Receiver<T> {
+impl<T> Drop for StaticReceiver<T> {
     fn drop(&mut self) {
-        self.inner.close_rx();
+        self.core.close_rx();
+    }
+}
+
+impl<T> fmt::Debug for StaticReceiver<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("StaticReceiver")
+            .field("core", &self.core)
+            .field("slots", &format_args!("&[..]"))
+            .finish()
     }
 }
 
 // === impl RecvRefFuture ===
 
+#[inline]
+fn poll_recv_ref<'a, T: Default>(
+    core: &'a ChannelCore<Waker>,
+    slots: &'a [Slot<T>],
+    cx: &mut Context<'_>,
+) -> Poll<Option<RecvRef<'a, T>>> {
+    core.poll_recv_ref(slots, || cx.waker().clone())
+        .map(|some| {
+            some.map(|slot| RecvRef {
+                _notify: super::NotifyTx(&core.tx_wait),
+                slot,
+            })
+        })
+}
+
 impl<'a, T: Default> Future for RecvRefFuture<'a, T> {
     type Output = Option<RecvRef<'a, T>>;
 
     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
-        self.rx.poll_recv_ref(cx)
+        poll_recv_ref(self.core, self.slots, cx)
     }
 }
 
@@ -282,14 +517,100 @@ impl<'a, T: Default> Future for RecvFuture<'a, T> {
     type Output = Option<T>;
 
     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
-        self.rx.poll_recv(cx)
+        poll_recv_ref(self.core, self.slots, cx)
+            .map(|opt| opt.map(|mut r| r.with_mut(core::mem::take)))
+    }
+}
+
+// === impl SendRefFuture ===
+
+impl<'sender, T: Default + 'sender> Future for SendRefFuture<'sender, T>
+where
+    T: Default + 'sender,
+{
+    type Output = Result<SendRef<'sender, T>, Closed>;
+
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        test_println!("SendRefFuture::poll({:p})", self);
+
+        loop {
+            let this = self.as_mut().project();
+            let node = this.waiter;
+            match test_dbg!(*this.state) {
+                State::Start => {
+                    match this.core.try_send_ref(this.slots) {
+                        Ok(slot) => return Poll::Ready(Ok(SendRef(slot))),
+                        Err(TrySendError::Closed(_)) => return Poll::Ready(Err(Closed(()))),
+                        Err(_) => {}
+                    }
+
+                    let start_wait = this.core.tx_wait.start_wait(node, cx.waker());
+
+                    match test_dbg!(start_wait) {
+                        WaitResult::Closed => {
+                            // the channel closed while we were registering the waiter!
+                            *this.state = State::Done;
+                            return Poll::Ready(Err(Closed(())));
+                        }
+                        WaitResult::Wait => {
+                            // okay, we are now queued to wait.
+                            // gotosleep!
+                            *this.state = State::Waiting;
+                            return Poll::Pending;
+                        }
+                        WaitResult::Notified => continue,
+                    }
+                }
+                State::Waiting => {
+                    let continue_wait = this.core.tx_wait.continue_wait(node, cx.waker());
+
+                    match test_dbg!(continue_wait) {
+                        WaitResult::Closed => {
+                            *this.state = State::Done;
+                            return Poll::Ready(Err(Closed(())));
+                        }
+                        WaitResult::Wait => return Poll::Pending,
+                        WaitResult::Notified => {
+                            *this.state = State::Done;
+                        }
+                    }
+                }
+                State::Done => match this.core.try_send_ref(this.slots) {
+                    Ok(slot) => return Poll::Ready(Ok(SendRef(slot))),
+                    Err(TrySendError::Closed(_)) => return Poll::Ready(Err(Closed(()))),
+                    Err(_) => {
+                        *this.state = State::Start;
+                    }
+                },
+            }
+        }
+    }
+}
+
+#[pin_project::pinned_drop]
+impl<T> PinnedDrop for SendRefFuture<'_, T> {
+    fn drop(self: Pin<&mut Self>) {
+        test_println!("SendRefFuture::drop({:p})", self);
+        let this = self.project();
+        if test_dbg!(*this.state) == State::Waiting && test_dbg!(this.waiter.is_linked()) {
+            this.waiter.remove(&this.core.tx_wait)
+        }
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl<T> fmt::Debug for Inner<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Inner")
+            .field("core", &self.core)
+            .field("slots", &format_args!("Box<[..]>"))
+            .finish()
     }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::ThingBuf;
 
     fn _assert_sync<T: Sync>(_: T) {}
     fn _assert_send<T: Send>(_: T) {}
@@ -297,7 +618,7 @@ mod tests {
     #[test]
     fn recv_ref_future_is_send() {
         fn _compiles() {
-            let (_, rx) = channel::<usize>(ThingBuf::new(10));
+            let (_, rx) = channel::<usize>(10);
             _assert_send(rx.recv_ref());
         }
     }
@@ -305,7 +626,7 @@ mod tests {
     #[test]
     fn recv_ref_future_is_sync() {
         fn _compiles() {
-            let (_, rx) = channel::<usize>(ThingBuf::new(10));
+            let (_, rx) = channel::<usize>(10);
             _assert_sync(rx.recv_ref());
         }
     }
@@ -313,7 +634,7 @@ mod tests {
     #[test]
     fn send_ref_future_is_send() {
         fn _compiles() {
-            let (tx, _) = channel::<usize>(ThingBuf::new(10));
+            let (tx, _) = channel::<usize>(10);
             _assert_send(tx.send_ref());
         }
     }
@@ -321,7 +642,7 @@ mod tests {
     #[test]
     fn send_ref_future_is_sync() {
         fn _compiles() {
-            let (tx, _) = channel::<usize>(ThingBuf::new(10));
+            let (tx, _) = channel::<usize>(10);
             _assert_sync(tx.send_ref());
         }
     }

+ 346 - 69
src/mpsc/sync.rs

@@ -6,18 +6,22 @@
 use super::*;
 use crate::{
     loom::{
-        atomic::{self, Ordering},
+        atomic::{self, AtomicBool, Ordering},
         sync::Arc,
         thread::{self, Thread},
     },
     wait::queue,
-    Ref, ThingBuf,
+    Ref,
 };
-use core::fmt;
+use core::{fmt, pin::Pin};
 
 /// Returns a new asynchronous multi-producer, single consumer channel.
-pub fn channel<T>(thingbuf: ThingBuf<T>) -> (Sender<T>, Receiver<T>) {
-    let inner = Arc::new(Inner::new(thingbuf));
+pub fn channel<T>(capacity: usize) -> (Sender<T>, Receiver<T>) {
+    let slots = (0..capacity).map(|_| Slot::empty()).collect();
+    let inner = Arc::new(Inner {
+        core: ChannelCore::new(capacity),
+        slots,
+    });
     let tx = Sender {
         inner: inner.clone(),
     };
@@ -27,12 +31,71 @@ pub fn channel<T>(thingbuf: ThingBuf<T>) -> (Sender<T>, Receiver<T>) {
 
 #[derive(Debug)]
 pub struct Sender<T> {
-    inner: Arc<Inner<T, Thread>>,
+    inner: Arc<Inner<T>>,
 }
 
 #[derive(Debug)]
 pub struct Receiver<T> {
-    inner: Arc<Inner<T, Thread>>,
+    inner: Arc<Inner<T>>,
+}
+
+/// A statically-allocated, blocking bounded MPSC channel.
+///
+/// A statically-allocated channel allows using a MPSC channel without
+/// requiring _any_ heap allocations. The [asynchronous variant][async] may be
+/// used in `#![no_std]` environments without requiring `liballoc`. This is a
+/// synchronous version which requires the Rust standard library, because it
+/// blocks the current thread in order to wait for send capacity. However, in
+/// some cases, it may offer _very slightly_ better performance than the
+/// non-static blocking channel due to requiring fewer heap pointer
+/// dereferences.
+///
+/// In order to use a statically-allocated channel, a `StaticChannel` must
+/// be constructed in a `static` initializer. This reserves storage for the
+/// channel's message queue at compile-time. Then, at runtime, the channel
+/// is [`split`] into a [`StaticSender`]/[`StaticReceiver`] pair in order to
+/// be used.
+///
+/// # Examples
+///
+/// ```
+/// use thingbuf::mpsc::StaticChannel;
+///
+/// // Construct a statically-allocated channel of `usize`s with a capacity
+/// // of 16 messages.
+/// static MY_CHANNEL: StaticChannel<usize, 16> = StaticChannel::new();
+///
+/// fn main() {
+///     // Split the `StaticChannel` into a sender-receiver pair.
+///     let (tx, rx) = MY_CHANNEL.split();
+///
+///     // Now, `tx` and `rx` can be used just like any other async MPSC
+///     // channel...
+/// # drop(tx); drop(rx);
+/// }
+/// ```
+///
+/// [async]: crate::mpsc::StaticChannel
+/// [`split`]: StaticChannel::split
+pub struct StaticChannel<T, const CAPACITY: usize> {
+    core: ChannelCore<Thread>,
+    slots: [Slot<T>; CAPACITY],
+    is_split: AtomicBool,
+}
+
+pub struct StaticSender<T: 'static> {
+    core: &'static ChannelCore<Thread>,
+    slots: &'static [Slot<T>],
+}
+
+pub struct StaticReceiver<T: 'static> {
+    core: &'static ChannelCore<Thread>,
+    slots: &'static [Slot<T>],
+}
+
+struct Inner<T> {
+    core: super::ChannelCore<Thread>,
+    slots: Box<[Slot<T>]>,
 }
 
 impl_send_ref! {
@@ -43,56 +106,111 @@ impl_recv_ref! {
     pub struct RecvRef<Thread>;
 }
 
+// === impl StaticChannel ===
+
+#[cfg(not(all(loom, test)))]
+impl<T, const CAPACITY: usize> StaticChannel<T, CAPACITY> {
+    const SLOT: Slot<T> = Slot::empty();
+
+    /// Constructs a new statically-allocated, blocking bounded MPSC channel.
+    ///
+    /// A statically-allocated channel allows using a MPSC channel without
+    /// requiring _any_ heap allocations. The [asynchronous variant][async] may be
+    /// used in `#![no_std]` environments without requiring `liballoc`. This is a
+    /// synchronous version which requires the Rust standard library, because it
+    /// blocks the current thread in order to wait for send capacity. However, in
+    /// some cases, it may offer _very slightly_ better performance than the
+    /// non-static blocking channel due to requiring fewer heap pointer
+    /// dereferences.
+    ///
+    /// In order to use a statically-allocated channel, a `StaticChannel` must
+    /// be constructed in a `static` initializer. This reserves storage for the
+    /// channel's message queue at compile-time. Then, at runtime, the channel
+    /// is [`split`] into a [`StaticSender`]/[`StaticReceiver`] pair in order to
+    /// be used.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use thingbuf::mpsc::StaticChannel;
+    ///
+    /// // Construct a statically-allocated channel of `usize`s with a capacity
+    /// // of 16 messages.
+    /// static MY_CHANNEL: StaticChannel<usize, 16> = StaticChannel::new();
+    ///
+    /// fn main() {
+    ///     // Split the `StaticChannel` into a sender-receiver pair.
+    ///     let (tx, rx) = MY_CHANNEL.split();
+    ///
+    ///     // Now, `tx` and `rx` can be used just like any other async MPSC
+    ///     // channel...
+    /// # drop(tx); drop(rx);
+    /// }
+    /// ```
+    ///
+    /// [async]: crate::mpsc::StaticChannel
+    /// [`split`]: StaticChannel::split
+    pub const fn new() -> Self {
+        Self {
+            core: ChannelCore::new(CAPACITY),
+            slots: [Self::SLOT; CAPACITY],
+            is_split: AtomicBool::new(false),
+        }
+    }
+
+    /// Split a [`StaticChannel`] into a [`StaticSender`]/[`StaticReceiver`]
+    /// pair.
+    ///
+    /// A static channel can only be split a single time. If
+    /// [`StaticChannel::split`] or [`StaticChannel::try_split`] have been
+    /// called previously, this method will panic. For a non-panicking version
+    /// of this method, see [`StaticChannel::try_split`].
+    ///
+    /// # Panics
+    ///
+    /// If the channel has already been split.
+    pub fn split(&'static self) -> (StaticSender<T>, StaticReceiver<T>) {
+        self.try_split().expect("channel already split")
+    }
+
+    /// Try to split a [`StaticChannel`] into a [`StaticSender`]/[`StaticReceiver`]
+    /// pair, returning `None` if it has already been split.
+    ///
+    /// A static channel can only be split a single time. If
+    /// [`StaticChannel::split`] or [`StaticChannel::try_split`] have been
+    /// called previously, this method returns `None`.
+    pub fn try_split(&'static self) -> Option<(StaticSender<T>, StaticReceiver<T>)> {
+        self.is_split
+            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
+            .ok()?;
+        let tx = StaticSender {
+            core: &self.core,
+            slots: &self.slots[..],
+        };
+        let rx = StaticReceiver {
+            core: &self.core,
+            slots: &self.slots[..],
+        };
+        Some((tx, rx))
+    }
+}
+
 // === impl Sender ===
 
 impl<T: Default> Sender<T> {
     pub fn try_send_ref(&self) -> Result<SendRef<'_, T>, TrySendError> {
-        self.inner.try_send_ref().map(SendRef)
+        self.inner
+            .core
+            .try_send_ref(self.inner.slots.as_ref())
+            .map(SendRef)
     }
 
     pub fn try_send(&self, val: T) -> Result<(), TrySendError<T>> {
-        self.inner.try_send(val)
+        self.inner.core.try_send(self.inner.slots.as_ref(), val)
     }
 
     pub fn send_ref(&self) -> Result<SendRef<'_, T>, Closed> {
-        // fast path: avoid getting the thread and constructing the node if the
-        // slot is immediately ready.
-        match self.inner.try_send_ref() {
-            Ok(slot) => return Ok(SendRef(slot)),
-            Err(TrySendError::Closed(_)) => return Err(Closed(())),
-            _ => {}
-        }
-
-        let mut waiter = queue::Waiter::new();
-        let mut unqueued = true;
-        let thread = thread::current();
-        loop {
-            let node = unsafe {
-                // Safety: in this case, it's totally safe to pin the waiter, as
-                // it is owned uniquely by this function, and it cannot possibly
-                // be moved while this thread is parked.
-                Pin::new_unchecked(&mut waiter)
-            };
-
-            let wait = if unqueued {
-                test_dbg!(self.inner.tx_wait.start_wait(node, &thread))
-            } else {
-                test_dbg!(self.inner.tx_wait.continue_wait(node, &thread))
-            };
-
-            match wait {
-                WaitResult::Closed => return Err(Closed(())),
-                WaitResult::Notified => match self.inner.try_send_ref() {
-                    Ok(slot) => return Ok(SendRef(slot)),
-                    Err(TrySendError::Closed(_)) => return Err(Closed(())),
-                    _ => {}
-                },
-                WaitResult::Wait => {
-                    unqueued = false;
-                    thread::park();
-                }
-            }
-        }
+        send_ref(&self.inner.core, self.inner.slots.as_ref())
     }
 
     pub fn send(&self, val: T) -> Result<(), Closed<T>> {
@@ -108,7 +226,7 @@ impl<T: Default> Sender<T> {
 
 impl<T> Clone for Sender<T> {
     fn clone(&self) -> Self {
-        test_dbg!(self.inner.tx_count.fetch_add(1, Ordering::Relaxed));
+        test_dbg!(self.inner.core.tx_count.fetch_add(1, Ordering::Relaxed));
         Self {
             inner: self.inner.clone(),
         }
@@ -117,14 +235,14 @@ impl<T> Clone for Sender<T> {
 
 impl<T> Drop for Sender<T> {
     fn drop(&mut self) {
-        if test_dbg!(self.inner.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
+        if test_dbg!(self.inner.core.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
             return;
         }
 
         // if we are the last sender, synchronize
         test_dbg!(atomic::fence(Ordering::SeqCst));
-        if self.inner.thingbuf.core.close() {
-            self.inner.rx_wait.close_tx();
+        if self.inner.core.core.close() {
+            self.inner.core.rx_wait.close_tx();
         }
     }
 }
@@ -133,24 +251,97 @@ impl<T> Drop for Sender<T> {
 
 impl<T: Default> Receiver<T> {
     pub fn recv_ref(&self) -> Option<RecvRef<'_, T>> {
-        loop {
-            match self.inner.poll_recv_ref(thread::current) {
-                Poll::Ready(r) => {
-                    return r.map(|slot| RecvRef {
-                        _notify: super::NotifyTx(&self.inner.tx_wait),
-                        slot,
-                    })
-                }
-                Poll::Pending => {
-                    test_println!("parking ({:?})", thread::current());
-                    thread::park();
-                }
+        recv_ref(&self.inner.core, self.inner.slots.as_ref())
+    }
+
+    pub fn recv(&self) -> Option<T> {
+        let val = self.recv_ref()?.with_mut(core::mem::take);
+        Some(val)
+    }
+
+    pub fn is_closed(&self) -> bool {
+        test_dbg!(self.inner.core.tx_count.load(Ordering::SeqCst)) <= 1
+    }
+}
+
+impl<'a, T: Default> Iterator for &'a Receiver<T> {
+    type Item = RecvRef<'a, T>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.recv_ref()
+    }
+}
+
+impl<T> Drop for Receiver<T> {
+    fn drop(&mut self) {
+        self.inner.core.close_rx();
+    }
+}
+
+// === impl StaticSender ===
+
+impl<T: Default> StaticSender<T> {
+    pub fn try_send_ref(&self) -> Result<SendRef<'_, T>, TrySendError> {
+        self.core.try_send_ref(self.slots).map(SendRef)
+    }
+
+    pub fn try_send(&self, val: T) -> Result<(), TrySendError<T>> {
+        self.core.try_send(self.slots, val)
+    }
+
+    pub fn send_ref(&self) -> Result<SendRef<'_, T>, Closed> {
+        send_ref(self.core, self.slots)
+    }
+
+    pub fn send(&self, val: T) -> Result<(), Closed<T>> {
+        match self.send_ref() {
+            Err(Closed(())) => Err(Closed(val)),
+            Ok(mut slot) => {
+                slot.with_mut(|slot| *slot = val);
+                Ok(())
             }
         }
     }
+}
+
+impl<T> Clone for StaticSender<T> {
+    fn clone(&self) -> Self {
+        test_dbg!(self.core.tx_count.fetch_add(1, Ordering::Relaxed));
+        Self {
+            core: self.core,
+            slots: self.slots,
+        }
+    }
+}
+
+impl<T> Drop for StaticSender<T> {
+    fn drop(&mut self) {
+        if test_dbg!(self.core.tx_count.fetch_sub(1, Ordering::Release)) > 1 {
+            return;
+        }
+
+        // if we are the last sender, synchronize
+        test_dbg!(atomic::fence(Ordering::SeqCst));
+        if self.core.core.close() {
+            self.core.rx_wait.close_tx();
+        }
+    }
+}
 
-    pub fn try_recv_ref(&self) -> Option<Ref<'_, T>> {
-        self.inner.thingbuf.pop_ref()
+impl<T> fmt::Debug for StaticReceiver<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("StaticReceiver")
+            .field("core", &self.core)
+            .field("slots", &format_args!("&[..]"))
+            .finish()
+    }
+}
+
+// === impl Receiver ===
+
+impl<T: Default> StaticReceiver<T> {
+    pub fn recv_ref(&self) -> Option<RecvRef<'_, T>> {
+        recv_ref(self.core, self.slots)
     }
 
     pub fn recv(&self) -> Option<T> {
@@ -159,11 +350,11 @@ impl<T: Default> Receiver<T> {
     }
 
     pub fn is_closed(&self) -> bool {
-        test_dbg!(self.inner.tx_count.load(Ordering::SeqCst)) <= 1
+        test_dbg!(self.core.tx_count.load(Ordering::SeqCst)) <= 1
     }
 }
 
-impl<'a, T: Default> Iterator for &'a Receiver<T> {
+impl<'a, T: Default> Iterator for &'a StaticReceiver<T> {
     type Item = RecvRef<'a, T>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -171,8 +362,94 @@ impl<'a, T: Default> Iterator for &'a Receiver<T> {
     }
 }
 
-impl<T> Drop for Receiver<T> {
+impl<T> Drop for StaticReceiver<T> {
     fn drop(&mut self) {
-        self.inner.close_rx();
+        self.core.close_rx();
+    }
+}
+
+impl<T> fmt::Debug for StaticSender<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("StaticSender")
+            .field("core", &self.core)
+            .field("slots", &format_args!("&[..]"))
+            .finish()
+    }
+}
+
+// === impl Inner ===
+
+impl<T> fmt::Debug for Inner<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Inner")
+            .field("core", &self.core)
+            .field("slots", &format_args!("Box<[..]>"))
+            .finish()
+    }
+}
+
+#[inline]
+fn recv_ref<'a, T: Default>(
+    core: &'a ChannelCore<Thread>,
+    slots: &'a [Slot<T>],
+) -> Option<RecvRef<'a, T>> {
+    loop {
+        match core.poll_recv_ref(slots, thread::current) {
+            Poll::Ready(r) => {
+                return r.map(|slot| RecvRef {
+                    _notify: super::NotifyTx(&core.tx_wait),
+                    slot,
+                })
+            }
+            Poll::Pending => {
+                test_println!("parking ({:?})", thread::current());
+                thread::park();
+            }
+        }
+    }
+}
+
+#[inline]
+fn send_ref<'a, T: Default>(
+    core: &'a ChannelCore<Thread>,
+    slots: &'a [Slot<T>],
+) -> Result<SendRef<'a, T>, Closed<()>> {
+    // fast path: avoid getting the thread and constructing the node if the
+    // slot is immediately ready.
+    match core.try_send_ref(slots) {
+        Ok(slot) => return Ok(SendRef(slot)),
+        Err(TrySendError::Closed(_)) => return Err(Closed(())),
+        _ => {}
+    }
+
+    let mut waiter = queue::Waiter::new();
+    let mut unqueued = true;
+    let thread = thread::current();
+    loop {
+        let node = unsafe {
+            // Safety: in this case, it's totally safe to pin the waiter, as
+            // it is owned uniquely by this function, and it cannot possibly
+            // be moved while this thread is parked.
+            Pin::new_unchecked(&mut waiter)
+        };
+
+        let wait = if unqueued {
+            test_dbg!(core.tx_wait.start_wait(node, &thread))
+        } else {
+            test_dbg!(core.tx_wait.continue_wait(node, &thread))
+        };
+
+        match wait {
+            WaitResult::Closed => return Err(Closed(())),
+            WaitResult::Notified => match core.try_send_ref(slots.as_ref()) {
+                Ok(slot) => return Ok(SendRef(slot)),
+                Err(TrySendError::Closed(_)) => return Err(Closed(())),
+                _ => {}
+            },
+            WaitResult::Wait => {
+                unqueued = false;
+                thread::park();
+            }
+        }
     }
 }

+ 11 - 12
src/mpsc/tests/mpsc_async.rs

@@ -8,7 +8,7 @@ use crate::{
 #[cfg_attr(ci_skip_slow_models, ignore)]
 fn mpsc_try_send_recv() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::new(3));
+        let (tx, rx) = channel(3);
 
         let p1 = {
             let tx = tx.clone();
@@ -41,7 +41,7 @@ fn mpsc_try_send_recv() {
 fn rx_closes() {
     const ITERATIONS: usize = 6;
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::new(ITERATIONS / 2));
+        let (tx, rx) = channel(ITERATIONS / 2);
 
         let producer = thread::spawn(move || {
             'iters: for i in 0..=ITERATIONS {
@@ -77,7 +77,7 @@ fn rx_closes() {
 #[test]
 fn spsc_recv_then_send() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<i32>::new(4));
+        let (tx, rx) = channel::<i32>(4);
         let consumer = thread::spawn(move || {
             future::block_on(async move {
                 assert_eq!(rx.recv().await.unwrap(), 10);
@@ -92,7 +92,7 @@ fn spsc_recv_then_send() {
 #[test]
 fn spsc_recv_then_close() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<i32>::new(4));
+        let (tx, rx) = channel::<i32>(4);
         let producer = thread::spawn(move || {
             drop(tx);
         });
@@ -109,7 +109,7 @@ fn spsc_recv_then_close() {
 #[test]
 fn spsc_recv_then_send_then_close() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<i32>::new(2));
+        let (tx, rx) = channel::<i32>(2);
         let consumer = thread::spawn(move || {
             future::block_on(async move {
                 assert_eq!(rx.recv().await.unwrap(), 10);
@@ -129,7 +129,7 @@ fn spsc_recv_then_send_then_close() {
 fn spsc_send_recv_in_order_no_wrap() {
     const N_SENDS: usize = 4;
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<usize>::new(N_SENDS));
+        let (tx, rx) = channel::<usize>(N_SENDS);
         let consumer = thread::spawn(move || {
             future::block_on(async move {
                 for i in 1..=N_SENDS {
@@ -151,7 +151,7 @@ fn spsc_send_recv_in_order_no_wrap() {
 fn spsc_send_recv_in_order_wrap() {
     const N_SENDS: usize = 2;
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<usize>::new(N_SENDS / 2));
+        let (tx, rx) = channel::<usize>(N_SENDS / 2);
         let consumer = thread::spawn(move || {
             future::block_on(async move {
                 for i in 1..=N_SENDS {
@@ -173,7 +173,7 @@ fn spsc_send_recv_in_order_wrap() {
 #[cfg_attr(ci_skip_slow_models, ignore)]
 fn mpsc_send_recv_wrap() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<usize>::new(1));
+        let (tx, rx) = channel::<usize>(1);
         let producer1 = do_producer(tx.clone(), 10);
         let producer2 = do_producer(tx, 20);
 
@@ -207,7 +207,7 @@ fn mpsc_send_recv_wrap() {
 #[test]
 fn mpsc_send_recv_no_wrap() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<usize>::new(2));
+        let (tx, rx) = channel::<usize>(2);
         let producer1 = do_producer(tx.clone(), 10);
         let producer2 = do_producer(tx, 20);
 
@@ -251,7 +251,7 @@ fn do_producer(tx: Sender<usize>, tag: usize) -> thread::JoinHandle<()> {
 #[test]
 fn tx_close_wakes() {
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::<i32>::new(2));
+        let (tx, rx) = channel::<i32>(2);
         let consumer = thread::spawn(move || {
             future::block_on(async move {
                 assert_eq!(rx.recv().await, None);
@@ -266,7 +266,7 @@ fn tx_close_wakes() {
 fn tx_close_drains_queue() {
     const LEN: usize = 4;
     loom::model(|| {
-        let (tx, rx) = channel(ThingBuf::new(LEN));
+        let (tx, rx) = channel(LEN);
         let producer = thread::spawn(move || {
             future::block_on(async move {
                 for i in 0..LEN {
@@ -281,7 +281,6 @@ fn tx_close_drains_queue() {
             }
         });
 
-
         producer.join().unwrap();
     });
 }

+ 11 - 11
src/mpsc/tests/mpsc_sync.rs

@@ -16,7 +16,7 @@ use crate::{
 #[ignore]
 fn mpsc_try_send_recv() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::new(3));
+        let (tx, rx) = sync::channel(3);
 
         let p1 = {
             let tx = tx.clone();
@@ -47,7 +47,7 @@ fn mpsc_try_send_recv() {
 fn rx_closes() {
     const ITERATIONS: usize = 6;
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::new(ITERATIONS / 2));
+        let (tx, rx) = sync::channel(ITERATIONS / 2);
 
         let producer = thread::spawn(move || {
             'iters: for i in 0..=ITERATIONS {
@@ -81,7 +81,7 @@ fn rx_closes() {
 #[test]
 fn spsc_recv_then_try_send() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<i32>::new(4));
+        let (tx, rx) = sync::channel::<i32>(4);
         let consumer = thread::spawn(move || {
             assert_eq!(rx.recv().unwrap(), 10);
         });
@@ -94,7 +94,7 @@ fn spsc_recv_then_try_send() {
 #[test]
 fn spsc_recv_then_close() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<i32>::new(4));
+        let (tx, rx) = sync::channel::<i32>(4);
         let producer = thread::spawn(move || {
             drop(tx);
         });
@@ -108,7 +108,7 @@ fn spsc_recv_then_close() {
 #[test]
 fn spsc_recv_then_try_send_then_close() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<i32>::new(2));
+        let (tx, rx) = sync::channel::<i32>(2);
         let consumer = thread::spawn(move || {
             assert_eq!(rx.recv().unwrap(), 10);
             assert_eq!(rx.recv().unwrap(), 20);
@@ -134,7 +134,7 @@ fn spsc_recv_then_try_send_then_close() {
 #[ignore]
 fn mpsc_send_recv_wrap() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<usize>::new(1));
+        let (tx, rx) = sync::channel::<usize>(1);
         let producer1 = do_producer(tx.clone(), 10);
         let producer2 = do_producer(tx, 20);
 
@@ -165,7 +165,7 @@ fn mpsc_send_recv_wrap() {
 #[test]
 fn mpsc_send_recv_no_wrap() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<usize>::new(2));
+        let (tx, rx) = sync::channel::<usize>(2);
         let producer1 = do_producer(tx.clone(), 10);
         let producer2 = do_producer(tx, 20);
 
@@ -205,7 +205,7 @@ fn do_producer(tx: sync::Sender<usize>, tag: usize) -> thread::JoinHandle<()> {
 fn spsc_send_recv_in_order_no_wrap() {
     const N_SENDS: usize = 4;
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<usize>::new(N_SENDS));
+        let (tx, rx) = sync::channel::<usize>(N_SENDS);
         let consumer = thread::spawn(move || {
             for i in 1..=N_SENDS {
                 assert_eq!(rx.recv(), Some(i));
@@ -234,7 +234,7 @@ fn spsc_send_recv_in_order_no_wrap() {
 fn spsc_send_recv_in_order_wrap() {
     const N_SENDS: usize = 2;
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<usize>::new(N_SENDS / 2));
+        let (tx, rx) = sync::channel::<usize>(N_SENDS / 2);
         let consumer = thread::spawn(move || {
             for i in 1..=N_SENDS {
                 assert_eq!(rx.recv(), Some(i));
@@ -253,7 +253,7 @@ fn spsc_send_recv_in_order_wrap() {
 #[test]
 fn tx_close_wakes() {
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::<i32>::new(2));
+        let (tx, rx) = sync::channel::<i32>(2);
         let consumer = thread::spawn(move || {
             assert_eq!(rx.recv(), None);
         });
@@ -266,7 +266,7 @@ fn tx_close_wakes() {
 fn tx_close_drains_queue() {
     const LEN: usize = 4;
     loom::model(|| {
-        let (tx, rx) = sync::channel(ThingBuf::new(LEN));
+        let (tx, rx) = sync::channel(LEN);
         let producer = thread::spawn(move || {
             for i in 0..LEN {
                 tx.send(i).unwrap();

+ 9 - 9
src/util/mutex.rs

@@ -1,17 +1,17 @@
-feature! {
-    #![all(feature = "std", not(feature = "parking_lot"))]
-    pub(crate) use self::std_impl::*;
-    mod std_impl;
-}
-
-#[cfg(not(feature = "std"))]
+#[cfg(not(any(feature = "std", all(loom, test))))]
 pub(crate) use self::spin_impl::*;
 
 #[cfg(any(not(feature = "std"), test))]
 mod spin_impl;
 
 feature! {
-    #![all(feature = "std", feature = "parking_lot")]
+    #![all(feature = "std", not(all(loom, test)))]
     #[allow(unused_imports)]
-    pub(crate) use parking_lot::{Mutex, MutexGuard};
+    pub(crate) use parking_lot::{Mutex, MutexGuard, const_mutex};
+}
+
+feature! {
+    #![all(loom, test)]
+    mod loom_impl;
+    pub(crate) use self::loom_impl::*;
 }

+ 2 - 11
src/util/mutex/std_impl.rs → src/util/mutex/loom_impl.rs

@@ -1,22 +1,13 @@
-#[cfg(all(test, loom))]
-use crate::loom::sync::Mutex as Inner;
-#[cfg(all(test, loom))]
 pub(crate) use crate::loom::sync::MutexGuard;
 
-#[cfg(not(all(test, loom)))]
-use std::sync::Mutex as Inner;
-
-#[cfg(not(all(test, loom)))]
-pub(crate) use std::sync::MutexGuard;
-
 use std::sync::PoisonError;
 
 #[derive(Debug)]
-pub(crate) struct Mutex<T>(Inner<T>);
+pub(crate) struct Mutex<T>(crate::loom::sync::Mutex<T>);
 
 impl<T> Mutex<T> {
     pub(crate) fn new(data: T) -> Self {
-        Self(Inner::new(data))
+        Self(crate::loom::sync::Mutex::new(data))
     }
 
     #[inline]

+ 8 - 0
src/util/mutex/spin_impl.rs

@@ -19,6 +19,14 @@ pub(crate) struct MutexGuard<'lock, T> {
     data: MutPtr<T>,
 }
 
+#[cfg(not(loom))]
+pub(crate) const fn const_mutex<T>(data: T) -> Mutex<T> {
+    Mutex {
+        locked: AtomicBool::new(false),
+        data: UnsafeCell::new(data),
+    }
+}
+
 impl<T> Mutex<T> {
     pub(crate) fn new(data: T) -> Self {
         Self {

+ 21 - 3
src/wait/queue.rs

@@ -3,7 +3,10 @@ use crate::{
         atomic::{AtomicUsize, Ordering::*},
         cell::UnsafeCell,
     },
-    util::{mutex::Mutex, CachePadded},
+    util::{
+        mutex::{self, Mutex},
+        CachePadded,
+    },
     wait::{Notify, WaitResult},
 };
 
@@ -156,7 +159,8 @@ const WAITING: usize = 1;
 const WAKING: usize = 2;
 const CLOSED: usize = 3;
 
-impl<T: Notify + Unpin> WaitQueue<T> {
+impl<T> WaitQueue<T> {
+    #[cfg(loom)]
     pub(crate) fn new() -> Self {
         Self {
             state: CachePadded(AtomicUsize::new(EMPTY)),
@@ -164,6 +168,16 @@ impl<T: Notify + Unpin> WaitQueue<T> {
         }
     }
 
+    #[cfg(not(loom))]
+    pub(crate) const fn new() -> Self {
+        Self {
+            state: CachePadded(AtomicUsize::new(EMPTY)),
+            list: mutex::const_mutex(List::new()),
+        }
+    }
+}
+
+impl<T: Notify + Unpin> WaitQueue<T> {
     /// Start waiting for a notification.
     ///
     /// If the queue has a stored notification, this consumes it and returns
@@ -478,6 +492,7 @@ impl<T> Waiter<T> {
     /// parameter ensures this method is only called while holding the lock, so
     /// this can be safe.
     #[inline(always)]
+    #[cfg_attr(loom, track_caller)]
     fn with_node<U>(&self, _list: &mut List<T>, f: impl FnOnce(&mut Node<T>) -> U) -> U {
         self.node.with_mut(|node| unsafe {
             // Safety: the dummy `_list` argument ensures that the caller has
@@ -489,6 +504,7 @@ impl<T> Waiter<T> {
     /// # Safety
     ///
     /// This is only safe to call while the list is locked.
+    #[cfg_attr(loom, track_caller)]
     unsafe fn set_prev(&mut self, prev: Option<NonNull<Waiter<T>>>) {
         self.node.with_mut(|node| (*node).prev = prev);
     }
@@ -496,6 +512,7 @@ impl<T> Waiter<T> {
     /// # Safety
     ///
     /// This is only safe to call while the list is locked.
+    #[cfg_attr(loom, track_caller)]
     unsafe fn take_prev(&mut self) -> Option<NonNull<Waiter<T>>> {
         self.node.with_mut(|node| (*node).prev.take())
     }
@@ -503,6 +520,7 @@ impl<T> Waiter<T> {
     /// # Safety
     ///
     /// This is only safe to call while the list is locked.
+    #[cfg_attr(loom, track_caller)]
     unsafe fn take_next(&mut self) -> Option<NonNull<Waiter<T>>> {
         self.node.with_mut(|node| (*node).next.take())
     }
@@ -514,7 +532,7 @@ unsafe impl<T: Send> Sync for Waiter<T> {}
 // === impl List ===
 
 impl<T> List<T> {
-    fn new() -> Self {
+    const fn new() -> Self {
         Self {
             head: None,
             tail: None,

+ 2 - 2
tests/debug.rs

@@ -1,8 +1,8 @@
-use thingbuf::{mpsc, ThingBuf};
+use thingbuf::mpsc;
 
 #[test]
 fn mpsc_debug() {
-    let (tx, rx) = mpsc::channel(ThingBuf::new(4));
+    let (tx, rx) = mpsc::channel(4);
     println!("tx={:#?}", tx);
     println!("rx={:#?}", rx);
     let _ = tx.try_send(1);

+ 2 - 2
tests/mpsc_async.rs

@@ -1,4 +1,4 @@
-use thingbuf::{mpsc, ThingBuf};
+use thingbuf::mpsc;
 
 #[tokio::test(flavor = "multi_thread")]
 async fn basically_works() {
@@ -18,7 +18,7 @@ async fn basically_works() {
         println!("PRODUCER {} DONE!", n);
     }
 
-    let (tx, rx) = mpsc::channel(ThingBuf::new(N_SENDS / 2));
+    let (tx, rx) = mpsc::channel(N_SENDS / 2);
     for n in 0..N_PRODUCERS {
         tokio::spawn(do_producer(tx.clone(), n));
     }

+ 3 - 3
tests/mpsc_sync.rs

@@ -1,5 +1,5 @@
 use std::thread;
-use thingbuf::{mpsc::sync, ThingBuf};
+use thingbuf::mpsc::sync;
 
 #[test]
 fn basically_works() {
@@ -24,7 +24,7 @@ fn basically_works() {
             .expect("spawning threads should succeed")
     }
 
-    let (tx, rx) = sync::channel(ThingBuf::new(N_SENDS / 2));
+    let (tx, rx) = sync::channel(N_SENDS / 2);
     for n in 0..N_PRODUCERS {
         start_producer(tx.clone(), n);
     }
@@ -56,7 +56,7 @@ fn tx_close_drains_queue() {
     for i in 0..10000 {
         println!("\n\n--- iteration {} ---\n\n", i);
 
-        let (tx, rx) = sync::channel(ThingBuf::new(LEN));
+        let (tx, rx) = sync::channel(LEN);
         let producer = thread::spawn(move || {
             for i in 0..LEN {
                 tx.send(i).unwrap();

+ 96 - 0
tests/static_storage.rs

@@ -94,3 +94,99 @@ fn static_storage_stringbuf() {
         assert_eq!(ln.parse::<usize>(), Ok(n));
     }
 }
+
+#[tokio::test]
+async fn static_async_channel() {
+    use std::collections::HashSet;
+    use thingbuf::mpsc;
+
+    const N_PRODUCERS: usize = 8;
+    const N_SENDS: usize = N_PRODUCERS * 2;
+
+    static CHANNEL: mpsc::StaticChannel<usize, N_PRODUCERS> = mpsc::StaticChannel::new();
+
+    async fn do_producer(tx: mpsc::StaticSender<usize>, n: usize) {
+        let tag = n * N_SENDS;
+        for i in 0..N_SENDS {
+            let msg = i + tag;
+            println!("sending {}...", msg);
+            tx.send(msg).await.unwrap();
+            println!("sent {}!", msg);
+        }
+        println!("PRODUCER {} DONE!", n);
+    }
+
+    let (tx, rx) = CHANNEL.split();
+    for n in 0..N_PRODUCERS {
+        tokio::spawn(do_producer(tx.clone(), n));
+    }
+    drop(tx);
+
+    let mut results = HashSet::new();
+    while let Some(val) = {
+        println!("receiving...");
+        rx.recv().await
+    } {
+        println!("received {}!", val);
+        results.insert(val);
+    }
+
+    let results = dbg!(results);
+
+    for n in 0..N_PRODUCERS {
+        let tag = n * N_SENDS;
+        for i in 0..N_SENDS {
+            let msg = i + tag;
+            assert!(results.contains(&msg), "missing message {:?}", msg);
+        }
+    }
+}
+
+#[test]
+fn static_sync_channel() {
+    use std::collections::HashSet;
+    use thingbuf::mpsc::sync;
+
+    const N_PRODUCERS: usize = 8;
+    const N_SENDS: usize = N_PRODUCERS * 2;
+
+    static CHANNEL: sync::StaticChannel<usize, N_PRODUCERS> = sync::StaticChannel::new();
+
+    fn do_producer(tx: sync::StaticSender<usize>, n: usize) -> impl FnOnce() {
+        move || {
+            let tag = n * N_SENDS;
+            for i in 0..N_SENDS {
+                let msg = i + tag;
+                println!("sending {}...", msg);
+                tx.send(msg).unwrap();
+                println!("sent {}!", msg);
+            }
+            println!("PRODUCER {} DONE!", n);
+        }
+    }
+
+    let (tx, rx) = CHANNEL.split();
+    for n in 0..N_PRODUCERS {
+        std::thread::spawn(do_producer(tx.clone(), n));
+    }
+    drop(tx);
+
+    let mut results = HashSet::new();
+    while let Some(val) = {
+        println!("receiving...");
+        rx.recv()
+    } {
+        println!("received {}!", val);
+        results.insert(val);
+    }
+
+    let results = dbg!(results);
+
+    for n in 0..N_PRODUCERS {
+        let tag = n * N_SENDS;
+        for i in 0..N_SENDS {
+            let msg = i + tag;
+            assert!(results.contains(&msg), "missing message {:?}", msg);
+        }
+    }
+}