# fanring
Fast typed MPSC and MPMC channels built from one SPSC `yring` per producer.
Sender registration is dynamic. Each sender writes to its own ring, so
producers do not contend on a shared queue tail. MPSC drains those rings
directly; MPMC stages prefetched batches in stealable receiver-local queues.
`Deferred` teardown is the default and avoids cleanup coordination on sends.
Choose `Coordinated` to reclaim unread payloads independently of idle sender
lifetimes.
Requires Rust 1.93 or newer.
| Send | `try_send` | `send` | `send_timeout` | `send_deadline` |
| Receive | `try_recv` | `recv` | `recv_timeout` | `recv_deadline` |
## Performance
Median throughput and blocking wake latency on the system named in each
chart, measured with the `performance` governor and CPU turbo disabled.
Detailed throughput heatmaps cover thread counts and payload sizes.

#### Detailed MPSC

#### Detailed MPMC

#### MPSC wake latency

#### MPMC wake latency

## Teardown policies
`Deferred` is the default teardown policy: unread ring payloads may remain
alive until their sender drops. Choose `Coordinated` when queued replies,
permits, or buffers must be released independently of idle sender lifetimes:
```rust
use fanring::{mpsc, teardown::Coordinated};
let (tx, rx) = mpsc::channel_with_policy::<String, Coordinated>(256);
```
The policy is part of the endpoint types and inherited by cloned handles.
`Coordinated` teardown drains unread values; overlapping sends clean up late
publications when they resume. Receiver drop does not wait for paused producers.
The additional send coordination has a measurable cost:


The general comparison and wake-latency charts above use `Deferred` for fanring.
These policy charts compare `u64` traffic under both policies on the same system.
See [teardown contracts, reproduction, and observed behavior in other channels](doc/teardown.md).
The pinned bounded Crossbeam, Crossfire, Flume, Kanal, and Thingbuf versions all
retain unread payloads in our receiver-drop probe while senders remain alive.
This is a payload-lifetime distinction; successful send still does not guarantee
that application code processes the message.
## MPSC
```rust
use fanring::mpsc::channel;
let (mut tx0, mut rx) = channel(8);
let mut tx1 = tx0.try_clone().expect("receiver alive");
tx0.send("from sender 0").unwrap();
tx1.send("from sender 1").unwrap();
for _ in 0..2 {
println!("{}", rx.recv().unwrap());
}
assert_eq!(tx0.send("still open"), Ok(()));
drop(rx);
assert_eq!(tx0.send("closed").unwrap_err().into_inner(), "closed");
```
`mpsc` keeps receive-side batching entirely inside `recv`/`try_recv`. It
preserves FIFO within each sender lane and relaxes order across senders.
## MPMC
```rust
use fanring::mpmc::channel;
let (mut tx, mut rx0) = channel(256);
let mut rx1 = rx0.clone();
tx.send("work 0").unwrap();
tx.send("work 1").unwrap();
let a = rx0.recv().unwrap();
let b = rx1.recv().unwrap();
assert_ne!(a, b);
```
`mpmc` drains up to 64 values from a ready sender ring. With one receiver, the
remaining values stay in a private deque. Cloning publishes that deque before
the new receiver becomes visible. With multiple receivers, remaining values
enter bounded synchronized work queues, and competitors steal up to eight at a
time. Receiver drop republishes its buffered work. Ordering is relaxed. Moving
values into that second-stage queue costs more for large inline types; box large
payloads when move bandwidth dominates.
`mpmc::try_recv` may return a transient `Empty` while bounded lane maintenance
or another receiver moves work. `Disconnected` is final: all senders are gone,
sender rings and staged queues are drained, and no work publication is in
flight.
## Contract
- One bounded ring per dynamic sender; capacity is a per-sender HWM rounded up
to a power of two.
- MPSC preserves FIFO within each sender lane. MPMC ordering is relaxed and its
receiver staging can temporarily exceed sender-ring capacity.
- Nonblocking, blocking, timeout, and deadline operations are available.
Blocking operations spin briefly before parking.
- Dropping the last sender preserves buffered values for receivers to drain.
Last-receiver drop follows the chosen teardown policy. MPMC `Empty` may be
transient while receivers move internal work; `Disconnected` is final.
- Sender hot paths and MPSC batching avoid a shared queue lock. MPMC work
distribution and topology maintenance use mutexes; blocking operations may
park.
## Good Fit
- Many long-lived or frequently changing producers feeding one or more
consumers.
- MPSC callers need only per-producer FIFO; MPMC callers accept relaxed order.
- Per-producer HWM matches the desired backpressure model.
- Callers can use built-in parking or own the backoff policy around `try_*`.
## Bad Fit
- Need global FIFO or strict one-item round robin.
- Need one exact capacity shared across all producers.
- Need an exact total MPMC bound that includes receiver staging.
- Need async wakeups.
## Further reading
* More detail: [DESIGN.md](DESIGN.md)
* Development and benchmark reproduction: [DEVELOPMENT.md](DEVELOPMENT.md)
* Release history: [CHANGELOG.md](CHANGELOG.md)
## License
[ISC](LICENSE)