bounded(0) 的 send 一直不返回
主线位置:一条消息走 zero flavor 的那条岔路。机制在 zero:会合 channel。
flowchart TB
ts["try_send"] --> lk["拿锁"]
lk --> q{"receivers 里有正在等的接收方?"}
q -->|有| pair["配对:写进对方的 Packet,Ok"]
q -->|没有,已断开| dis["Disconnected"]
q -->|没有| full["Full:没有缓冲可放"]
snd["send"] --> reg["消息放在自己栈上的 Packet,挂进 senders,park"]
reg --> woke["接收方进入 recv 时选中它、读走消息"]
现象
想要「容量尽量小」的 channel,写了 bounded(0);或者在同一个线程里先 send 再 recv:
let (s, r) = crossbeam_channel::bounded(0);
s.send(1).unwrap(); // 永远不返回
let v = r.recv().unwrap();
send 卡住,没有报错。换成 try_send,又总是 Full,哪怕另一个线程里明明有接收端。
实测
exp-crossbeam/pitfalls/src/bin/zero_capacity.rs(crossbeam-channel 0.5.17,release,跑 3 次输出一致,耗时一列每次差几毫秒):
try_send, no receiver: Err("Full(..)")
send_timeout(100ms), no recv: Err("SendTimeoutError(..)") after 110.019167ms
len=0 capacity=Some(0) is_full=true
try_send, receiver alive but not waiting: Err("Full(..)")
send(4) returned after 52.292ms; receiver got Ok(4)
try_send, receiver parked in recv: Ok(()); receiver got Ok(5)
- 没有接收方:
try_send是Full,send_timeout超时(这里 100 ms 的超时 110 ms 才返回,见 超时了还成功)。send就是没有超时的这一种,会一直等下去。 - 第四行:另一个线程持有接收端,但 50 ms 之后才调
recv。这时try_send仍是Full:「有接收端」不等于「有接收方在等」。 - 第五行:阻塞的
send等到对方进入recv才返回,用了 52 ms。 - 第六行:接收方已经停在
recv里,try_send才成功。 is_full()永远是true,len()永远是 0。
从哪来
zero 的 try_send:
let mut inner = self.inner.lock();
// If there's a waiting receiver, pair up with it.
if let Some(operation) = inner.receivers.try_select() {
token.zero.0 = operation.packet;
drop(inner);
unsafe {
self.write(token, msg).ok().unwrap();
}
Ok(())
} else if inner.is_disconnected {
Err(TrySendError::Disconnected(msg))
} else {
Err(TrySendError::Full(msg))
}
只有 receivers 等待队列里有一个别的线程的、还在等的接收方,才能配对。没有缓冲可以先放进去,所以「先发后收」在单线程里是死锁,在多线程里是「发送方等接收方」。is_full 直接返回 true。阻塞的 send 把消息放在自己栈上的 Packet 里挂进 senders,然后 park,被接收方选中、读完以后才返回(zero)。
std 的 sync_channel(0) 是同一份代码的移植,行为相同,文档也写明它是会合 channel。
代价:慢约 50 倍
每条消息都要一方睡下、另一方叫醒,没有缓冲把两边错开。实验 E6:1P1C 下 bounded(0) 约 0.72 Mmsg/s,bounded(1024) 约 35.2,慢约 50 倍;4 个发送方时降到 0.30(zero)。
怎么改
- 要的是「容量很小的队列」:用
bounded(1)。它有一个槽,send在没人收时也能先放进去。 - 要的是「发送方必须等到对方接手」(握手、交接所有权、背压到底):
bounded(0)正是为此设计的,但发送和接收必须在不同线程,且不要在持有对方需要的锁时send。 - 要在没有接收方时不阻塞:
try_send并处理Full;但要知道 zero 上Full的意思是「此刻没有接收方停在recv里」,而不是「缓冲满了」。 - 在
select!里用 zero 的发送分支是可以的:它注册一个堆上的Packet,等任意一个接收方来配对(zero)。
小结
bounded(0)没有缓冲,send要等接收方正在recv;同一线程里先发后收会永远阻塞。try_send只在有接收方已经在等时成功,否则Full;is_full()永远为真。- 本机 1P1C 吞吐比
bounded(1024)低约 50 倍。要小容量就用bounded(1)。
💬 关联讨论
发起讨论加载中...