- 示例工程
- 教程
【免费下载链接】100-exercises-to-learn-rust
A self-paced course to learn Rust, one exercise at a time.
在 Rust 的多线程编程中,std::thread::spawn要求传入的闭包满足'static生命周期,否则借用检查器会以 "argument requires thatvis borrowed for'static"(详见 07_threads/02_static)为由拒绝编译。除了把数据按值移入线程外,还有一种绕过借用约束的经典手段:主动泄漏内存。本文以 100-exercises-to-learn-rust 仓库第 7 章第 3 节 Leaking data 为骨架,结合 03_leak 练习源码 与其测试用例,系统讲解Box::leak/Vec::leak的原理、危险边界(OOM)与 OS 回收机制,并给出"何时可以放心泄漏"的工程判断标准。
泄漏问题的根源:spawn 线程的'static约束
std::thread::spawn创建的线程是**脱离(detached)**的:它可能在父线程退出之后继续运行,直至整个进程结束。因此,Rust 编译器要求传给spawn的闭包必须满足'static约束——闭包捕获的值要么是自有的(owned),要么是引用但该引用在整个程序运行期间都有效:
pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T + Send + 'static, T: Send + 'static { // [..] }如果闭包捕获的是一个会在线程运行期间被释放的堆内存引用,就会产生经典的use-after-free(释放后使用)漏洞。这正是 02_static 中sum练习遇到的编译错误:v在函数返回时被 drop,而线程仍然持有指向其内部的切片引用。
核心方案:用Box::leak获得'static引用
处理堆分配数据时,你可以告诉 Rust"这块内存永远不会被回收",即故意泄漏(leak)内存,从而拿到一个'static引用。标准库的Box::leak正是为此设计:
// 在堆上分配一个 u32,用 Box 包裹 let x = Box::new(41u32); // 告诉 Rust 永远不会释放这块堆内存, // 从而拿到一个 'static 引用 let static_ref: &'static mut u32 = Box::leak(x);注意Box::leak返回的是&'static mut T——不仅生命周期延长到'static,还保留了可变性。这就让"把大块堆数据切分后分发给多个线程"成为可能,无需复制数据。
练习场景:Vec::leak切分堆内存并行求和
仓库中的 03_leak 练习 完整呈现了这个思路的实战形态。练习要求(见 src/lib.rs):
- 给定一个
Vec<i32>,泄漏其堆内存分配; - 将得到的静态切片切成两半;
- 在两个独立线程中分别对两半求和。
这正是 02_static 练习 中"禁止额外分配内存"约束的延续:上一个练习用static数组满足'static,这里则需要把运行期的堆数据"静态化"。
关键工具是Vec::leak,它可以一步把Vec<T>变成&'static mut [T](并顺便丢弃 Vec 的栈上元数据与容量信息,只保留堆内存本身):
use std::thread; pub fn sum(v: Vec<i32>) -> i32 { // 泄漏堆内存,获得 'static 切片 let static_slice: &'static mut [i32] = v.leak(); let mid = static_slice.len() / 2; let left = &static_slice[..mid]; let right = &static_slice[mid..]; let left_handle = thread::spawn(move || left.iter().sum::<i32>()); let right_handle = thread::spawn(move || right.iter().sum::<i32>()); left_handle.join().unwrap() + right_handle.join().unwrap() }测试验证
练习自带的测试(lib.rs 的 tests 模块)覆盖了空向量、单元素、5/9/10 个元素的求和结果:
sum(vec![]) == 0sum(vec![1]) == 1sum(vec![1, 2, 3, 4, 5]) == 15sum(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]) == 45sum(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) == 55
空切片情况下mid == 0,两个线程各处理空切片,求和自然为 0——Vec::leak对空 Vec 同样成立,不会产生空指针问题。该练习的 Cargo.toml 仅依赖标准库(edition = "2021"),无需任何外部 crate 即可cargo test验证。
内存泄漏是进程级的:警惕 OOM
泄漏数据是危险的:如果持续泄漏,进程最终会耗尽内存并以out-of-memory(OOM)错误崩溃。原文档给出了一个触发函数:
// 如果让它一直运行,最终会耗尽全部可用内存 fn oom_trigger() { loop { let v: Vec<usize> = Vec::with_capacity(1024); v.leak(); } }Vec::with_capacity(1024)一次性分配一块 1024 个usize的堆空间,随后leak让这块内存永远无法被释放;循环每次迭代又新增一块,内存占用单调递增,直到触发系统 OOM。
为什么泄漏不等于"永远丢失":OS 的进程级回收
与直觉相反,经leak泄漏的内存并非"真正被遗忘"。操作系统能够把每一块内存区域映射到负责它的进程:当进程退出时,操作系统会自动回收该进程的全部内存。所以"让 OS 去处理"(let the OS deal with it)在特定场景下是完全合理的内存管理策略。
工程判断:什么场景下"故意泄漏"是合理的
原文档给出两条判断标准,满足其一即可考虑使用泄漏方案:
- 需要泄漏的内存总量有界 / 提前可知:例如只泄漏固定数量、固定大小的数据(如上述练习中每个
Vec只泄漏一次,总量受输入规模约束),泄漏总量可控,不会无限增长。 - 进程生命周期短:确信进程在耗尽可用内存之前就会退出,例如 CLI 工具、批处理任务、测试程序等短暂进程。
对于长生命周期的服务进程(比如仓库后续章节打造的 多线程票务服务端,以及 通道版客户端-服务器架构),内存是持续累积的核心资源,应当避免任何形式的无界泄漏——这正是后续章节引入 scoped threads、channels、Arc等更安全方案的原因。
对比与演进:scoped threads 是更优雅的替代
值得强调的是,泄漏只是"让引用满足'static"的一种手段,并非唯一手段,也未必是最佳手段。本书下一节 Scoped threads 提供了更安全的替代方案:std::thread::scope创建的线程会在 scope 结束时自动 join,因此可以安全借用非'static的环境数据:
let v = vec![1, 2, 3]; let midpoint = v.len() / 2; std::thread::scope(|scope| { scope.spawn(|| { let first = &v[..midpoint]; println!("Here's the first half of v: {first:?}"); }); scope.spawn(|| { let second = &v[midpoint..]; println!("Here's the second half of v: {second:?}"); }); }); println!("Here's v: {v:?}");scope版本既不拷贝数据、也不泄漏内存,还能在 scope 返回后继续使用v。它和Box::leak/Vec::leak形成了鲜明的对照:当你需要一块"永久有效"的堆数据时选泄漏;当线程生命周期可以收束在某个作用域内时,scoped threads 是零代价且零风险的答案。
小结
围绕 Leaking data 一文,可以提炼出以下要点:
spawn要求闭包满足'static,这是避免 use-after-free 的编译期保障;Box::leak(以及Vec::leak)通过放弃回收堆内存,把普通引用升级为'static引用,适合把堆数据切分后送入多个线程;- 泄漏的内存并非永久丢失,进程退出时由操作系统统一回收;
- 无界泄漏会触发 OOM,只有在泄漏量有界或进程短命时才可接受"让 OS 兜底"的策略;
- 当线程能被限定在某个作用域内时,优先考虑
std::thread::scope,它是泄漏方案的更安全替代。
理解这些边界,是写出既通过编译又不会在生产环境"吃光内存"的并发 Rust 代码的第一步。下一步可以继续阅读本书后续章节:Channels、Locks 与 Send/Sync 语义,逐步构建完整的并发模型。
- 示例工程
- 教程
【免费下载链接】100-exercises-to-learn-rust
A self-paced course to learn Rust, one exercise at a time.
相关推荐
100-exercises-to-learn-rust:深入理解 Rust 生命周期(Lifetimes)与 `IntoIterator for &TicketStore`
100 exercises to learn rust:深入理解 Rust 生命周期(Lifetimes)与 IntoIterator for &TicketS
示例工程教程深入 Rust 并发基石:`'static` 生命周期与 `thread::spawn` 的借用约束(100-exercises-to-learn-rust)
深入 Rust 并发基石: 'static 生命周期与 thread::spawn 的借用约束(100 exercises to learn rust) 'st
示例工程教程Rust 生命周期实践:深入理解 'static 生命周期
Rust 生命周期实践:深入理解 'static 生命周期 什么是 'static 生命周期 在 Rust 中, 'static 是一个特殊的生命周期标记,表示
文档教程示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考