news 2026/10/3 8:17:29

100-exercises-to-learn-rust:理解 Rust 内存泄漏——用 `Box::leak` 与 `Vec::leak` 跨越 `‘static` 生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
100-exercises-to-learn-rust:理解 Rust 内存泄漏——用 `Box::leak` 与 `Vec::leak` 跨越 `‘static` 生命周期
  • 示例工程
  • 教程

【免费下载链接】100-exercises-to-learn-rust

A self-paced course to learn Rust, one exercise at a time.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载

在 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![]) == 0
  • sum(vec![1]) == 1
  • sum(vec![1, 2, 3, 4, 5]) == 15
  • sum(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]) == 45
  • sum(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)在特定场景下是完全合理的内存管理策略。

工程判断:什么场景下"故意泄漏"是合理的

原文档给出两条判断标准,满足其一即可考虑使用泄漏方案:

  1. 需要泄漏的内存总量有界 / 提前可知:例如只泄漏固定数量、固定大小的数据(如上述练习中每个Vec只泄漏一次,总量受输入规模约束),泄漏总量可控,不会无限增长。
  2. 进程生命周期短:确信进程在耗尽可用内存之前就会退出,例如 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.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载
上一篇:Edgartools批量处理技巧:高效下载并分析数千份SEC文件
下一篇:claude-seo 的 Google API 认证完全指南:从 API Key、Service Account 到 GA4 的 8 步配置与源码级验证

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:13:06

如何安全制作系统启动盘:Balena Etcher 镜像烧录全流程

如何安全制作系统启动盘&#xff1a;Balena Etcher 镜像烧录全流程 【免费下载链接】etcher Flash OS images to SD cards & USB drives, safely and easily. 项目地址: https://gitcode.com/GitHub_Trending/et/etcher 把系统镜像写进 SD 卡或 U 盘时&#xff0c;最…

作者头像 李华