send_timeout 的时间已经过了,消息还是发出去了
主线位置:一条消息的入口,阻塞、超时、截止时间三种接口在 flavor 里是同一个函数。机制在 API 与 flavor。
flowchart TB
call0["send_timeout(msg, d)"] --> st["start_send"]
st -->|成功| ok["Ok:不看截止时间"]
st -->|失败| bo["Backoff 自旋 / 让出约 11 次,期间成功也算 Ok"]
bo --> chk{"now >= deadline?"}
chk -->|是| to["Timeout(msg)"]
chk -->|否| pk["注册、park_timeout,醒来回到 start_send"]
pk --> st
现象
想用「截止时间已过就别再发」来丢弃过期的消息:
if s.send_deadline(msg, deadline).is_err() {
metrics.dropped += 1;
}
结果截止时间早就过了,消息照样发了出去,dropped 不涨。反过来,用 recv_timeout(Duration::ZERO) 当「非阻塞地看一眼」是可以的,但它和 try_recv 不完全一样(下面)。
实测
exp-crossbeam/pitfalls/src/bin/timeout_elapsed.rs,一个 bounded(1),按顺序执行(crossbeam-channel 0.5.17,release,跑 3 次输出相同):
send_timeout(ZERO), empty: Ok(())
send_timeout(ZERO), full: Err("SendTimeoutError(..)")
recv_deadline(past), 1 msg: Ok(1)
recv_timeout(ZERO), empty: Err(Timeout)
send_deadline(past), empty: Ok(())
recv_timeout(ZERO), 1 msg: Ok(3)
past 是一秒前的 Instant。只要不需要阻塞,超时参数根本没被看:空 channel 上 send_timeout(_, ZERO) 和 send_deadline(_, past) 都成功,有消息时 recv_deadline(past) 也成功。只有「需要等」的两行返回了超时。
另一面:超时返回的时刻也不精确。bounded(0) 那一页里 send_timeout(100ms) 用了 110 ms 才返回:阻塞前先自旋、让出约 11 次,然后 park_timeout,醒来再检查一次,这几段都会多花时间。
从哪来
array 的 send 每一轮先 start_send,失败才看截止时间(代码见 API 与 flavor);recv 对称。list 的 send 根本不看截止时间(无界,永远不满),zero 的 send 只要有接收方在等就立即配对。
0.5.17 把这个行为写进了文档。send_timeout(#1289):
If the channel is not full and the timeout has already elapsed, the message will still be sent.
recv_timeout(#1280):
If the channel is non-empty and the timeout has already elapsed, the next message in the channel will be returned.
send_deadline、recv_deadline 各有一句同样的话。代码没变,变的是承诺:这是有意的语义,不是会被修掉的 bug。
recv_timeout(ZERO) 和 try_recv 的区别:前者在空的时候会先走一遍 Backoff 自旋、让出(约 11 次 snooze),中间如果有消息到了就拿到;后者只试一次。想要「看一眼、绝不等」用 try_recv / try_send。
怎么改
- 「过期就丢」:在调用前自己看表,
if Instant::now() >= deadline { drop(msg) } else { s.send_deadline(msg, deadline) }。 - 「不阻塞」:用
try_send/try_recv,不要用零超时。 - 「最多等多久」:
send_timeout/recv_timeout的语义是「最多阻塞这么久」,不是「这么久以后一定返回」;实际返回可能晚几毫秒到十几毫秒,取决于平台的唤醒延迟(timers 里本机sleep睡过头约 1.3 ms,负载高时更多)。
小结
- 带超时的收发先尝试、后看表:截止时间已过,只要不用等,照样成功。
- 0.5.17 的文档把这一点写成了承诺(#1289、#1280)。
- 过期丢弃要自己在调用前判断;不阻塞用
try_*。