API 与 flavor
主线位置:一条消息的入口。
Sender::send按 flavor 分发到array::Channel::send,后面的事在 array 和 Context 与 Waker;channel 什么时候被释放由本页的counter.rs决定。
flowchart TB
b0["bounded(0)"] --> zero["SenderFlavor::Zero"]
bn["bounded(n), n > 0"] --> array["SenderFlavor::Array"]
ub["unbounded()"] --> list["SenderFlavor::List"]
af["after(d) / at(t)"] --> at["ReceiverFlavor::At"]
tk["tick(d)"] --> tick["ReceiverFlavor::Tick"]
nv["never()"] --> never["ReceiverFlavor::Never"]
zero --> cnt["counter::Sender / Receiver"]
array --> cnt
list --> cnt
cnt --> heap["一次 Box:Counter{senders, receivers, destroy, chan}"]
at --> arc["Arc:只有接收端"]
tick --> arc
构造函数与 flavor
公开 API 只有几个自由函数,每个都在 channel.rs 里,按参数挑一个 flavor:
| 函数 | flavor | 容量 | 源码 |
|---|---|---|---|
unbounded() | list | 无限 | channel.rs:45 |
bounded(0) | zero | 0,会合 | channel.rs:109 |
bounded(n),n > 0 | array | n | 同上 |
after(d) | at | 1,只投递一次 | channel.rs:176 |
at(t) | at | 1 | channel.rs:227 |
never() | never | 0,永不就绪 | channel.rs:268 |
tick(d) | tick | 1,周期投递 | channel.rs:327 |
bounded 的分支就是一个 if cap == 0:
pub fn bounded<T>(cap: usize) -> (Sender<T>, Receiver<T>) {
if cap == 0 {
let (s, r) = counter::new(flavors::zero::Channel::new());
...
} else {
let (s, r) = counter::new(flavors::array::Channel::with_capacity(cap));
...
}
}
after、tick 在 Instant::now() + d 溢出时(比如传 Duration::MAX)退化成 never(),不 panic。never 从 0.5.16 起是 const fn(#1250),可以放进 static。
Sender 三种,Receiver 六种
SenderFlavor 只有 Array、List、Zero;ReceiverFlavor 多出 At、Tick、Never。后三种没有发送端:消息不是谁发来的,是接收时按当前时间「造」出来的,类型固定是 Receiver<Instant>(never 是任意 T,反正永远收不到)。所以 Receiver::recv 里 At / Tick 两支要用 mem::transmute_copy 把 Result<Instant, _> 转成 Result<T, _>:泛型函数里编译器不知道 T 就是 Instant,只能靠构造函数保证。
每个公开方法都是同一个形状:match &self.flavor,每个 flavor 调自己的同名方法。例如 Sender::send:
pub fn send(&self, msg: T) -> Result<(), SendError<T>> {
match &self.flavor {
SenderFlavor::Array(chan) => chan.send(msg, None),
SenderFlavor::List(chan) => chan.send(msg, None),
SenderFlavor::Zero(chan) => chan.send(msg, None),
}
.map_err(|err| match err {
SendTimeoutError::Disconnected(msg) => SendError(msg),
SendTimeoutError::Timeout(_) => unreachable!(),
})
}
阻塞、带超时、带截止时间三种接口,在 flavor 里是同一个函数,区别只是 deadline 参数:send 传 None,send_timeout(d) 算出 now + d 再调 send_deadline,send_deadline 直接传 Some(deadline)。now + d 溢出时,send_timeout 退回不带超时的 send(0.5.7 的 #953「Improve handling of very large timeout」)。
flavor 的公共接口
每个存消息的 flavor 都实现同一组方法,没有 trait,靠 channel.rs 里的 match 约定:
| 方法 | 作用 |
|---|---|
start_send(&mut Token) -> bool / start_recv | 预订一个槽:成功时把槽的位置写进 Token,返回 true;断开也返回 true,Token 里放空指针 |
write(&mut Token, msg) / read(&mut Token) | 按 Token 写入 / 读出,然后叫醒对面 |
try_send / try_recv | start_* 一次,不阻塞 |
send(msg, deadline) / recv(deadline) | 循环:start_*,不行就自旋、注册、park |
len / capacity / is_empty / is_full / is_disconnected | 查询 |
disconnect* | 最后一个发送端或接收端 drop 时调 |
「预订」和「完成」分成两步,是为了 select:select! 先在多个 channel 上 start_*,选中一个后把 Token 交给调用方,调用方在分支里再 read / write。Token(select.rs:25)是一个结构体,每个 flavor 一个字段(at、array、list、never、tick、zero),谁用谁填。
counter.rs:谁来释放
array、list、zero 三种 flavor 的 channel 都包在 Counter 里,一次 Box 分配:
struct Counter<C> {
senders: AtomicUsize,
receivers: AtomicUsize,
destroy: AtomicBool,
chan: C,
}
counter::Sender<C> 和 counter::Receiver<C> 都只是一个裸指针。它没用 Arc,因为要分开数两边:channel 的「断开」由某一边的最后一个句柄触发,而释放内存要等两边都走光。
sequenceDiagram participant S as 最后一个 Sender participant C as Counter participant R as 最后一个 Receiver S->>C: senders.fetch_sub(1) == 1 S->>C: disconnect(chan):置 mark 位,叫醒对面 S->>C: destroy.swap(true) 返回 false:不释放 R->>C: receivers.fetch_sub(1) == 1 R->>C: disconnect(chan) R->>C: destroy.swap(true) 返回 true:Box::from_raw 释放
pub(crate) unsafe fn release<F: FnOnce(&C) -> bool>(&self, disconnect: F) {
if self.counter().senders.fetch_sub(1, Ordering::AcqRel) == 1 {
disconnect(&self.counter().chan);
if self.counter().destroy.swap(true, Ordering::AcqRel) {
drop(Box::from_raw(self.counter));
}
}
}
destroy 是一个「第二个到的人负责」的标志:两边的最后一个句柄各 swap(true) 一次,拿到旧值 true 的那个是后到的,它释放。AcqRel 保证先到的一方在 disconnect 里做的写,对释放的一方可见。
acquire(clone 时调)计数超过 isize::MAX 就 process::abort()(counter.rs:48),防的是「克隆后 mem::forget」把计数绕回 0,和 Arc 的做法一样。
disconnect 闭包传哪个,决定了最后一个接收端 drop 时消息是否立即析构。Receiver 的 Drop:
ReceiverFlavor::Array(chan) => chan.release(|c| c.disconnect()),
ReceiverFlavor::List(chan) => chan.release(|c| c.disconnect_receivers()),
ReceiverFlavor::Zero(chan) => chan.release(|c| c.disconnect()),
list 有专门的 disconnect_receivers,会顺手丢弃缓冲里的消息;array 只有一个通用的 disconnect,消息要等 Counter 被释放、Channel 的 Drop 才析构。实验 E8 在 array 里。
at / tick / never 不进 Counter:前两者是 Arc<flavors::at::Channel>,never 是零大小类型,Drop 里什么也不做。
错误类型
err.rs 和 std 的 mpsc 一一对应,发送类错误都把消息还给调用方:
| 操作 | 错误 | 变体 |
|---|---|---|
send | SendError<T>(pub T) | 只有「断开」一种 |
try_send | TrySendError<T> | Full(T)、Disconnected(T) |
send_timeout / send_deadline | SendTimeoutError<T> | Timeout(T)、Disconnected(T) |
recv | RecvError | 断开且已空 |
try_recv | TryRecvError | Empty、Disconnected |
recv_timeout / recv_deadline | RecvTimeoutError | Timeout、Disconnected |
Select::try_select 等 | TrySelectError、SelectTimeoutError、TryReadyError、ReadyTimeoutError | 单元结构体 |
接收侧的「断开」只在已经空了的时候报:发送端全走了,缓冲里还有的消息照样能收完。Receiver::iter() 就靠这个结束:recv 返回 Err 时迭代器返回 None。
超时了也可能成功
send_timeout 的文档在 0.5.17 补了一句(#1289,channel.rs:455):
If the channel is not full and the timeout has already elapsed, the message will still be sent.
recv_timeout / recv_deadline 也补了对称的一句(#1280、#1245):channel 非空时,即使截止时间已过,也返回下一条消息。
原因在 flavor 的循环顺序。array 的 send 每一轮先试 start_send,后看截止时间:
loop {
let backoff = Backoff::new();
loop {
if self.start_send(token) {
let res = unsafe { self.write(token, msg) };
return res.map_err(SendTimeoutError::Disconnected);
}
if backoff.is_completed() { break; } else { backoff.snooze(); }
}
if let Some(d) = deadline {
if Instant::now() >= d {
return Err(SendTimeoutError::Timeout(msg));
}
}
// 注册、park 到 deadline
}
所以 send_timeout(msg, Duration::ZERO) 不等于「保证不发」:有空位就发出去了。park_timeout 醒来以后也是先 start_send 再看时间,醒得晚了照样成功。想要「过了时间就别发」的语义,得在调用前自己看表;想要「不阻塞」,用 try_send。list 的 send 根本不看 deadline(无界,永远不满),zero 的 send 只要有接收方在等就立即配对。
bounded(1) 的 send_timeout(x, 0) 什么时候返回 Timeout?channel 空着时调用,会返回什么?channel 已满、且没有任何接收方时呢?如果另一个线程在你调用前 1 微秒刚 recv 走一条,结果又是什么?
动脑筋答案
- 空着:
start_send第一次就成功,返回Ok(()),超时参数根本没被看。 - 已满且没人收:内层循环自旋、
snooze到is_completed()(约 11 次,见 Backoff),这期间满的状态不变;出来后Instant::now() >= d,返回Timeout(x),消息原样还回来。 - 刚被收走一条:那一刻已经不满,第一次
start_send就成功,返回Ok(())。「超时」只在真的要阻塞时才会被检查。
小结
bounded(0)是 zero、bounded(n)是 array、unbounded()是 list;after/at/tick/never只有接收端,类型是Receiver<Instant>。- 公开方法一律
match flavor分发;阻塞、超时、截止时间三种接口在 flavor 里是同一个函数,只差deadline。 - 每个 flavor 把操作拆成
start_*(预订,结果写进Token)和read/write(完成),这是select能跨 channel 工作的前提。 Counter分开数发送端和接收端,某一边走光就disconnect,两边都走光时第二个到达的释放内存(destroy.swap(true))。- 最后一个接收端 drop 时,list 立即丢弃缓冲消息,array 和 zero 不会;0.5.17 的 array 要等发送端也走光。
- 带超时的收发先尝试、后看表:超时参数为 0 也可能成功。