DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

Rust 1.99 的 WASI 线程退出后 TLS 析构未执行,join 成功也不能证明资源释放

wasm32-wasip1-threads 上 join 成功却没执行 thread_local 对象的 Drop。核对 1.99 目标分支与析构计数,把必要清理放到工作线程可控制的生命周期,避免误认 1.99.1 已修复。

在 wasm32-wasip1-threads 上,工作线程已经结束,JoinHandle::join() 也返回成功,thread_local! 中对象的 Drop 却没有执行。如果这个对象承担释放外部资源的工作,线程池关闭后仍可能保留资源。

Rust 1.99.0 在这一目标上存在对应回归:1.98.1 能观察到线程退出时的析构,1.99.0 同样的探针得到零次。已核对的 1.99.1 相关源码仍相同,不能因为补丁号变了就认定 TLS 析构也已修复。

范围必须限定到带线程的这个 WASI 目标。wasm32-wasip1、浏览器 Wasm、原生 Linux 和 macOS 的线程退出实现不同,不应统一归因。

用析构计数区分线程结束与清理完成

在隔离的小程序或已有测试中,让每个工作线程第一次访问 TLS 时放入一个带 Drop 的探针,析构时增加共享原子计数;等待所有线程 join 后读取计数。这样检查的是清理是否发生,而不是线程是否有运行日志。

探针可以采用这样的类型片段:

use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};

struct Probe(Arc<AtomicUsize>);

impl Drop for Probe {
    fn drop(&mut self) {
        self.0.fetch_add(1, Ordering::SeqCst);
    }
}

需要把探针确实放入每个线程访问过的 TLS 槽中。仅声明 thread_local!、却没有访问它,不会触发需要析构的实例。也要排除 mem::forget、进程直接终止与 panic=abort 等本来就可能跳过清理的路径。

对照必须使用同一个支持 wasi-threads 的运行器和启动参数。已有明确故障组合使用 Wasmtime 35.0.0;该版与后续运行器的线程开关不同,不能复制旧命令给一个已经移除对应开关的新版本,再把参数错误当成 TLS 问题。

1.99 的目标选择落入了不执行析构的分支

标准库通过目标条件决定如何在线程退出时触发 TLS 析构。1.99.0 调整 WASI 支撑时,把部分目标重新归入暂不执行这项清理的实现;带 atomics 的 wasm32-wasip1-threads 也被包含进去。

因此线程可以正常结束,等待线程的代码也能恢复,缺失的只是应在退出时调用的清理。增加 join 次数或在线程结束后等待更久,不会替这个分支注册析构函数。

后续主线已修正 WASI 的选择条件,但这项源码修正与 1.99.1 修复 UEFI wcslen 是两件事。采用新工具链前,要查目标标准库是否含 TLS 修正,不能只看相关 issue 已关闭。

将必须执行的清理移到可控制的生命周期

若资源必须在线程任务结束前释放,避免把唯一清理责任留给这个受影响目标的 TLS 退出钩子。可以让工作线程在自己的正常退出路径显式取出 TLS 对象并释放,或让资源由线程任务中的普通局部对象持有。

例如工程原本使用下面的槽时,正常工作结束后可以在同一工作线程内取出对象:

use std::cell::Cell;

thread_local! {
    static PROBE: Cell<Option<Probe>> = const { Cell::new(None) };
}

fn clear_probe_on_current_thread() {
    PROBE.with(|slot| drop(slot.take()));
}

这段沿用前面的 Probe。take() 将槽置空并把对象交给 drop,不会再留下同一个实例等退出时重复释放。真实资源的释放规则仍由它自己的实现决定,不能在父线程调用这个函数来清理所有工作线程;父线程只会访问自己的 TLS。

显式调用还要纳入业务提前返回和错误路径。把资源改成局部 RAII 对象时同样应检查提前退出、panic 策略及外部资源关闭要求。若应用仍依赖 TLS 自动析构,可在兼容性允许时固定到已验证的 1.98.1 暂避,再规划工具链更新;不要在没有测试的情况下回退整套依赖。

验证应覆盖多个工作线程和多轮线程池启停,分别检查析构次数与实际外部资源状态。加入显式清理后计数正常,只证明该处理有效;要验证工具链本身修复,还需保留不主动清空 TLS 的独立探针。