DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

Tokio recv_many 在关闭通道上 panic,检查 Vec 中已有的数据

Tokio 1.50.0 的 mpsc recv_many 在关闭通道、非空 Vec 的特定路径错误断言。采用正式修复或独立批次缓冲,保留原有消息。

Tokio 的 mpsc 接收端已经 close(),队列也没有消息,调用 recv_many(&mut buffer, limit) 却在库内部 panic。如果传入的 Vec 里保留了上一批数据,堆栈可能指向 tokio::sync::mpsc::chan 的空缓冲区断言。

Tokio 1.50.0 存在这条具体故障:关闭且空闲的通道路径错误地要求整个 Vec 为空。recv_many 本来允许向已有 Vec 追加消息;没有新消息时,应保留原内容并返回零,不应因为调用前已有元素而崩溃。

close 不会清空调用方的 Vec

下面是独立的异步诊断函数,工程需要启用 Tokio 的 sync 功能,并由已有 runtime 调用:

use tokio::sync::mpsc;

async fn closed_channel_probe() {
    let (_tx, mut rx) = mpsc::channel::<i32>(1);
    let mut buffer = vec![7];

    rx.close();
    let received = rx.recv_many(&mut buffer, 1).await;

    assert_eq!(received, 0);
    assert_eq!(buffer, vec![7]);
}

这里的断言描述正确行为,不是运行记录。1.50.0 的受影响路径会在到达这些断言前 panic。无界 mpsc 的相应接收路径也有同类问题。

close() 只阻止新的普通发送,不丢弃已经排队的消息;关闭前取得的发送 permit 仍可能产生消息。因此,真实程序不能在 close() 后假定第一次接收必定返回零。上面的例子没有排队消息,也没有 outstanding permit,才具备这个前提。

使用包含修正的正式版本

Tokio 1.51.0 的正式发行记录已列出这项修复,1.51.1 也保留了修正:源码比较 Vec 的长度是否仍等于本次调用开始时的长度,而非要求它原本为空。1.47.4 的维护分支也记录了同项回补;这不代表任意数字更大的旧维护版本都包含补丁。

在项目允许的版本范围内采用包含修复的版本,核对 Cargo.lock 和实际构建结果。工作区可能存在多版 Tokio,应确认出错组件使用的那一版,而不只检查根 Cargo.toml。保留锁文件差异,验证后再更新部署产物。

暂时不能升级时,可让每次接收使用一个新的空批次缓冲区,再把本批结果交给业务处理。示例针对有界 mpsc:

use tokio::sync::mpsc;

async fn drain(mut rx: mpsc::Receiver<i32>, collected: &mut Vec<i32>) {
    loop {
        let mut batch = Vec::with_capacity(32);
        let received = rx.recv_many(&mut batch, 32).await;
        if received == 0 {
            break;
        }
        collected.extend(batch);
    }
}

collected 是示例中的结果集合,长期流不能无限积累,应换成应用原有的消费、写入或有界存储。若复用批次 Vec,必须在该批数据已经被业务取走后再清空,不能为避开断言丢掉未处理消息。每轮新建 Vec 还会增加分配开销,应视作过渡办法。

用返回数量判断结束

接收的数量是本次追加的消息数,不是 Vec 的总长度。升级后应分别验证非空 Vec 接空通道、Vec 中已有数据再追加消息,以及关闭后仍有队列消息的情况。limit 为零会直接返回零,不能把这种调用当作通道已经耗尽的证据。

若程序在 recv_many 外层仍然 panic,检查是否是自己的索引、消费逻辑或锁毒化;不要对所有 panic 统一增加 catch_unwind。本题修复的是一个内部断言,正常的关闭、排空和 permit 生命周期仍需要应用按原语义处理。