1. 核心工具链
|---------------|----------------------|-----------------------------------|
| 工具名 | 主要功能 | 常用命令示例 |
| Cargo | 包管理、构建、测试、发布 | cargo new, cargo build, cargo run |
| rustc | Rust 编译器 | rustc main.rs |
| rustup | 工具链安装与管理 | rustup update |
| Clippy | 代码 lint,发现常见错误和优化点 | cargo clippy |
| Rustfmt | 代码格式化 | cargo fmt |
| rust-analyzer | IDE 语言服务器,提供代码补全、跳转等 | (通常集成在编辑器中) |
1.1. Cargo
Cargo 是 Rust 的构建系统和包管理器,是日常开发中最常用的工具。它帮助你:
创建项目 :cargo new project_name创建一个新项目,cargo new --lib project_name创建一个库项目
管理依赖 :在 Cargo.toml的 [dependencies]下添加包(crate)和版本,Cargo 会从 crates.io(Rust 官方包注册中心)或 Git 仓库等下载并编译它们
构建项目:
cargo build:编译项目
cargo build --release:进行发布构建,启用优化,生成更小更快的二进制文件,但编译时间更长
运行与检查:
cargo run:编译并运行项目
cargo check:快速检查代码能否通过编译,而不生成可执行文件,速度很快
运行测试 :cargo test会运行项目中所有的测试函数
生成文档 :cargo doc会为你的项目和其依赖生成 HTML 文档
1.2. rustc
rustc是 Rust 的编译器,负责将 Rust 源代码编译成可执行文件或库。虽然我们通常通过 Cargo 来调用 rustc,但直接使用它可以帮助你理解编译过程或进行一些简单的编译任务
1.3. rustup
rustup用于安装和管理多个 Rust 工具链
你可以:
- 安装 stable(稳定版)、beta(测试版)和 nightly(夜间版) 三种发布通道的 Rust
- 轻松地更新 Rust:
rustup update - 为不同的项目切换不同的工具链版本。
- 安装不同平台的标准库,用于交叉编译。
2. VSCode开发环境搭建
Ubuntu 22.04.5 LTS + vscode ssh
由于rustup官方服务器在国外
如果直接按照rust官网的安装方式安装非常容易失败,即使不失败也非常非常慢
如果用国内的镜像则可以分分钟就搞定
1. curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rust.sh
2. vim rust.sh
// RUSTUP_UPDATE_ROOT编辑为
// RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup"
// 这是用来下载 rustup-init 的, 修改后通过国内镜像下载
3. export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
// 这让 rustup-init从国内进行下载rust的组件,提高速度
4. bash rust.sh
// 默认选1
5. vi $HOME/.cargo/env
// 末尾新增
// RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
// 为了以后都从国内镜像源下载包
6. source $HOME/.cargo/env
7. rustc --version
// rustc 1.89.0 (29483883e 2025-08-04)
vscode上面安装如下插件

vscode验证一哈
$ cargo new hello_world
Created binary (application) `hello_world` package
$ cd hello_world
$ cargo run
Compiling hello_world v0.1.0 (/home/skylink/code/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 0.75s
Running `target/debug/hello_world`
Hello, world!

3. Rust优势
- Rust 是一门静态编译语言,其功能定位与 C++ 相似
rustc使用 LLVM 作为它的后端。
- Rust 支持多种平台和架构:
- x86、ARM、WebAssembly......
- Linux、Mac、Windows......
- Rust 被广泛用于各种设备中:
- 固件和引导程序,
- 智能显示器,
- 手机,
- 桌面,
- 服务器。
独有的优势如下:
- 你可以达到堪比 C 和 C++ 的性能,而没有内存不安全的问题
○ 不存在未初始化的变量。
○ 不存在“双重释放”。
○ 不存在“释放后使用”。
○ 不存在 NULL 指针。
○ 不存在被遗忘的互斥锁。
○ 不存在线程之间的数据竞争。
○ 不存在迭代器失效。
- 没有未定义的运行时行为:每个 Rust 语句的行为都有明确定义
○ 数组访问有边界检查。
○ 整数溢出有明确定义(panic 或回绕)。
- 现代语言功能:具有与高级语言一样丰富且人性化的表达能力
○ 枚举和模式匹配。
○ 泛型。
○ 无额外开销的外部函数接口(FFI)。
○ 零成本抽象。
○ 强大的编译器错误提示。----后面我们就能看到了,真的强
○ 内置依赖管理器。
○ 对测试的内置支持。
○ 优秀的语言服务协议(Language Server Protocol)支持。
4. 令人印象深刻的语法
由于本人的技术栈是C/C++/Go/Python,所以这里只介绍一些Rust让我吃惊的语法 ,高度相似的语法我就不介绍了,详细的请看这个:https://www.bookstack.cn/read/comprehensive-rust-202412-zh/c01514b4d45220e0.md
4.1. 变量默认是不可变的
fn main() {
let x: i32 = 10;
println!("x: {x}");
x = 20;
println!("x: {x}");
}
root@aqnlc-ubuntu-134-93:/home/skylink/code/hello_world# cargo run
Compiling hello_world v0.1.0 (/home/skylink/code/hello_world)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x: i32 = 10;
| - first assignment to `x`
3 | println!("x: {x}");
4 | x = 20;
| ^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x: i32 = 10;
| +++
// 编译报错了,并且编译器还给出了提示,让我们用mut修饰可变变量
4.2. 变量类型是明确的
虽然你可以不写,让编译器去推导,但是推导出来的所生成的机器码与明确类型声明完全相同。整形默认是i32,浮点字面量默认为 f64
fn main() {
let x = 3.14;
let y = 20;
assert_eq!(x, y);
}
error[E0277]: can't compare `{float}` with `{integer}`
--> src/main.rs:4:5
|
4 | assert_eq!(x, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{float} == {integer}`
4.3. Lables
continue 和 break 都可以选择接受一个标签参数,用来 终止嵌套循环
fn main() {
// 定义一个 3x3 的二维数组 `s`
let s = [[5, 6, 7], [8, 9, 10], [21, 15, 32]];
// 计数器,记录搜索过的元素个数
let mut elements_searched = 0;
// 要查找的目标值
let target_value = 10;
// 使用带标签的 'outer 循环来遍历行(i 从 0 到 2)
'outer: for i in 0..=2 {
// 内层循环遍历列(j 从 0 到 2)
for j in 0..=2 {
// 每检查一个元素,计数器加 1
elements_searched += 1;
// 判断当前元素是否等于目标值
if s[i][j] == target_value {
// 如果找到目标值,立即跳出外层循环('outer)
break 'outer;
}
}
}
// 打印最终搜索过的元素数量
println!("elements searched: {elements_searched}");
}
4.4. 分号: 表达式和语句分不清楚?
表达式(Expression)会计算并产生一个值
语句 (Statement)执行操作但不返回值,其类型永远是单元类型 ()
在代码块中,如果最后一行以分号结尾,它就成了语句,如果最后一行没有分号,它就是一个表达式
fn main() {
let z = 13;
let x = {
let y = 10;
println!("y: {y}");
z - y; // 应该是 z-y
};
println!("x: {x}");
}
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> src/main.rs:8:19
|
6 | z - y;
| - help: remove this semicolon
7 | };
8 | println!("x: {x}");
| -^-
| ||
| |`()` cannot be formatted with the default formatter
| required by this formatting parameter
4.5. 宏:可变参数的函数
rust函数始终采用固定数量的参数。不支持默认参数。宏可用于支持可变函数。
println!(format, ..)prints a line to standard output, applying formatting described in std::fmt.format!(format, ..)的用法与println!类似,但它以字符串形式返回结果。dbg!(expression)会记录表达式的值并返回该值。todo!()用于标记尚未实现的代码段。如果执行该代码段,则会触发 panic。unreachable!()用于标记无法访问的代码段。如果执行该代码段,则会触发 panic。
fn factorial(n: u32) -> u32 {
let mut product = 1;
for i in 1..=n {
product *= dbg!(i);
}
product
}
fn fizzbuzz(n: u32) -> u32 {
todo!()
}
fn main() {
let n = 4;
println!("{n}! = {}", factorial(n));
}
[src/main.rs:4:20] i = 1
[src/main.rs:4:20] i = 2
[src/main.rs:4:20] i = 3
[src/main.rs:4:20] i = 4
4! = 24
4.6. 数组的长度也是类型的一部分
[u8; 3] and [u8; 4] are considered two different types.
fn main() {
let mut a: [i8; 10] = [42; 10];
a[5] = 0;
println!("a: {a:?}");
}
4.7. 所有权转移,独占引用和共享引用
Rust 通过共享引用 (&T) 和独占引用 (&mut T) 的严格区分,在编译期就杜绝了数据竞争的可能性。
- 当你只需要读取 数据,并且希望多个部分都能同时访问时,使用共享引用。
- 当你需要修改 数据时,必须使用独占引用,并且要遵守其"独占"的规则。
如果不加&符号,这意味着函数会获取数据的所有权,调用后原始数据就不能再使用了。
// ❌ 如果不用 &,会发生所有权转移
fn magnitude(v: [f64]) -> f64 { ... }
fn main() {
let arr = [1.0, 2.0, 3.0];
let result = magnitude(arr); // arr 的所有权被转移给函数
// println!("{:?}", arr); // ❌ 编译错误!arr 已经不能使用
}
// ✅ 使用 & 进行借用
fn magnitude(v: &[f64]) -> f64 { ... }
fn main() {
let arr = [1.0, 2.0, 3.0];
let result = magnitude(&arr); // 只是借用,不转移所有权
println!("{:?}", arr); // ✅ arr 仍然可以使用
}
共享引用不允许修改数据,而独占引用允许。
fn main() {
let mut x = 5;
// 共享引用 - 只读
let r1 = &x;
let r2 = &x; // 可以同时有多个共享引用
println!("r1 = {}, r2 = {}", r1, r2); // 允许
// *r1 = 10; // 错误!不能通过共享引用修改数据 [2](@ref)
// 独占引用 - 可读写
let m1 = &mut x;
*m1 = 10; // 允许修改
println!("x = {}", x); // x 现在为 10
// let m2 = &mut x; // 错误!同一作用域内不能有第二个独占引用 [1,2](@ref)
// println!("r1 = {}", r1); // 错误!在存在可变引用后,之前的共享引用也不能再使用
}
Rust 的借用检查器严格执行"独占"规则。
fn main() {
let mut data = vec![1, 2, 3];
// 创建一个共享引用
let shared_ref = &data[0];
println!("First element: {}", shared_ref);
// 在共享引用最后一次使用后,可以创建独占引用
let mutable_ref = &mut data;
mutable_ref.push(4); // 允许修改
// println!("First element: {}", shared_ref);
// 错误!在可变借用后不能再使用之前的共享引用 [6](@ref)
}
在函数签名中明确使用引用可以避免所有权的转移。
// 使用共享引用作为参数:只读,不获取所有权
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里 s 离开作用域,但由于它是引用,不拥有所有权,所以不会丢弃任何数据 [7](@ref)
// 使用独占引用作为参数:可修改,不获取所有权
fn modify_string(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传递共享引用
println!("The length of '{}' is {}.", s, len); // s 仍然有效
let mut s_mut = String::from("hello");
modify_string(&mut s_mut); // 传递独占引用
println!("Modified string: {}", s_mut); // 输出 "hello, world!"
}
4.8. 不一样的枚举enum
和C++的命名整型常量完全不一样,更像是 C++中的类
// C++
enum class Color { Red, Green, Blue }; // 每个成员仅代表一个整数值
// Color myColor = Color::Red;
// RUST
enum Message {
Quit, // 无关联数据
Move { x: i32, y: i32 }, // 匿名结构体
Write(String), // 一个 String
ChangeColor(i32, i32, i32), // 三个 i32
}
let msg = Message::Write(String::from("Hello"));
这种设计让 Rust 的枚举非常适合用于构建状态机、处理事件或消息
- 模式匹配
选择 if let :当你只关心一种匹配情况,并且对其他情况不感兴趣或只需简单处理时
选择 let else :当你需要解构一个值并获取其内部数据 ,但如果解构失败需要立即报告错误或提前返回时。它能有效减少嵌套,让主逻辑更突出
选择 while let :当你需要持续地从某个操作(如迭代器、弹出栈等)中提取值,直到该操作无法再产生匹配模式的值时
别忘了 match :当你需要处理所有可能的情况 时,match仍然是必不可少的选择,因为它强制进行穷尽性检查
// match
#[rustfmt::skip]
fn main() {
let input = 'x';
match input {
'q' => println!("Quitting"),
'a' | 's' | 'w' | 'd' => println!("Moving around"),
'0'..='9' => println!("Number input"),
key if key.is_lowercase() => println!("Lowercase: {key}"),
_ => println!("Something else"),
}
}
// if let
fn sleep_for(secs: f32) {
if let Ok(dur) = Duration::try_from_secs_f32(secs) {
std::thread::sleep(dur);
println!("slept for {:?}", dur);
}
}
// let else
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let s = if let Some(s) = maybe_string {
s
} else {
return Err(String::from("got None"));
};
}
// while let
// Some(c)是 Option枚举的一个变体,表示“有一个值,这个值是 c”。
fn main() {
let mut name = String::from("Comprehensive Rust 🦀");
while let Some(c) = name.pop() {
println!("character: {c}");
}
// (There are more efficient ways to reverse a string!)
}
4.9. trait特征
和golang的接口很像,区别是Rust :必须显式 使用 impl Trait for Type来声明一个类型实现了某个 trait。这是一种意图的明确声明,代码可读性更强,编译器也能更早地捕获错误。
trait Pet {
fn talk(&self) -> String;
fn greet(&self) {
println!("Oh you're a cutie! What's your name? {}", self.talk());
}
}
struct Dog {
name: String,
age: i8,
}
impl Pet for Dog {
fn talk(&self) -> String {
format!("Woof, my name is {}!", self.name)
}
}
关联类型
和泛型很像,区别就是更简洁,无需在 trait 名称或方法调用中暴露额外的类型参数
// trait 定义:使用关联类型 Item
trait Contains {
type Item; // 关联类型占位符
fn contains(&self, element: Self::Item) -> bool;
}
struct MyContainer(i32);
// 为 MyContainer 实现 Contains trait
impl Contains for MyContainer {
type Item = i32; // 指定关联类型为 i32
fn contains(&self, element: Self::Item) -> bool {
self.0 == element
}
}
// trait 定义:使用泛型参数 E (Element)
trait Contains<E> {
fn contains(&self, element: E) -> bool;
}
// 一个简单的结构体,包装一个 i32
struct MyContainer(i32);
// 为 MyContainer 实现 Contains trait,指定泛型 E 为 i32
impl Contains<i32> for MyContainer {
fn contains(&self, element: i32) -> bool {
self.0 == element
}
}
4.10. 派生功能是通过宏实现的
Rust 中的 #[derive(...)]是一种属性宏(Attribute Macro),用于让编译器自动为你的类型(结构体、枚举或联合体)实现指定的 trait。它极大地简化了代码,避免了手动编写重复的样板代码。
|---------|------------------------------------------------------------------------|
| Trait | 作用 |
| Debug | 生成用于调试输出的代码,允许使用 {:?}或 {:#?}格式化符号打印结构体内容。 |
| Clone | 提供 .clone()方法,用于创建值的深拷贝(deep copy)。这意味着会完整复制结构体及其所有数据(包括 String等堆上数据)。 |
| Default | 提供 ::default()关联函数,返回一个所有字段均为默认值的实例。 |
#[derive(Debug, Clone, Default)]
struct Player {
name: String,
strength: u8,
hit_points: u8,
}
fn main() {
let p1 = Player::default(); // Default trait adds `default` constructor.
let mut p2 = p1.clone(); // Clone trait adds `clone` method.
p2.name = String::from("EldurScrollz");
// Debug trait adds support for printing with `{:?}`.
println!("{:?} vs. {:?}", p1, p2);
}
// Player { name: "", strength: 0, hit_points: 0 } vs. Player { name: "EldurScrollz", strength: 0, hit_points: 0 }
4.11. 特征边界
特征边界是 Rust 泛型编程的核心概念之一,用于约束泛型类型必须实现某些特定的行为(即特征)。
// 要求类型 T 必须实现 Clone trait
// 写法1
fn duplicate<T: Clone>(a: T) -> (T, T) {
(a.clone(), a.clone())
}
// 写法2
fn duplicate<T>(a: T) -> (T, T)
where
T: Clone, // 使用 where 子句声明约束
{
(a.clone(), a.clone())
}
// 写法3
fn duplicate(a: impl Clone) -> (impl Clone, impl Clone) { // 参数和返回值位置都可以用
(a.clone(), a.clone())
}
4.12. 重要的内置enum类型
Option和Result
在许多编程语言中,经常用 null或 nil来表示"无值"。但直接访问一个 null引用常常导致运行时错误(空指针异常)。Rust 通过 Option<T>类型,在编译阶段就强制你必须处理值可能为 None的情况,从而避免了这类运行时错误
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = name.find('é'); // 查找字符返回 Option<usize>
println!("find returned {position:?}"); // 打印: find returned Some(14)
assert_eq!(position.unwrap(), 14); // 通过 unwrap 取出 Some 中的值
position = name.find('Z'); // 查找一个不存在的字符
println!("find returned {position:?}"); // 打印: find returned None
// assert_eq!(position.expect("Character not found"), 0); // 如果为 None,expect 会 panic 并附带错误信息
match position {
Some(idx) => println!("Found at index: {}", idx),
None => println!("Character not found."),
}
Result 是 Rust 中用于处理可恢复错误的核心枚举类型。它强制开发者显式处理操作可能成功或失败的场景
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("diary.txt");
match file {
Ok(mut file) => { // 文件打开成功,file 绑定为 File 类型
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Dear diary: {contents} ({bytes} bytes)");
} else {
println!("Could not read file content");
}
}
Err(err) => { // 文件打开失败,err 绑定为 std::io::Error 类型
println!("The diary could not be opened: {err}");
}
}
}
4.13. 内部可变性Cell和 RefCell
内部可变性是一种设计模式,它允许你在仅持有数据的不可变引用时,也能修改数据本身 。这听起来似乎违反了 Rust 的基本借用规则,但 Cell和 RefCell通过一些巧妙的机制(或是在编译时确保安全,或是在运行时动态检查)使得这一操作变得安全。
个典型的场景是实现某个外部定义的 Trait 。假设一个库定义了一个消息发送接口,出于设计考虑,它要求 send方法接收不可变引用 &self:
// 外部库定义的Trait,我们无法修改
pub trait Messenger {
fn send(&self, msg: String); // 注意:这里是 &self,不是 &mut self
}
但你在实现这个Trait时,需要将消息加入一个内部缓存队列。这时,你就需要在 &self的方法内部修改数据。如果没有内部可变性,这是不可能的。而 RefCell就派上了用场 :
use std::cell::RefCell;
struct MyMessenger {
message_cache: RefCell<Vec<String>>, // 使用RefCell包裹缓存
}
impl Messenger for MyMessenger {
fn send(&self, msg: String) {
// 在不可变引用&self的方法内,通过borrow_mut获取可变引用并修改数据
self.message_cache.borrow_mut().push(msg);
}
}
Cell的话适用于简单的数据结构,类似于bool,整型啥的:
use std::cell::Cell;
struct Person {
age: Cell<u32>, // 即使Person实例不可变,age字段也能变
}
impl Person {
fn have_birthday(&self) { // 注意:这里接收的是不可变引用 &self
let current_age = self.age.get();
self.age.set(current_age + 1); // 但可以修改Cell内部的字段
}
}
fn main() {
let person = Person { age: Cell::new(30) }; // person本身不是mut
person.have_birthday();
println!("Age after birthday: {}", person.age.get());
}
4.14. 生命周期
Rust 的生命周期是其所有权系统的重要组成部分,核心目标是在编译期确保引用始终有效 ,从而避免悬垂指针等内存安全问题。当编译器无法自动推断引用关系时,需要显式标注生命周期 。生命周期参数以撇号开头,通常使用短小的名称(如 'a)。
// case 1
// Highlight 注释会强制包含 &str 的底层数据的生命周期,
// 至少与使用该数据的任何 Highlight 实例一样长。
// 如果 text 在 fox(或 dog)的生命周期结束前被消耗,借用检查器将抛出一个错误。
#[derive(Debug)]
struct Highlight<'doc>(&'doc str);
fn erase(text: String) {
println!("Bye {text}!");
}
fn main() {
let text = String::from("The quick brown fox jumps over the lazy dog.");
let fox = Highlight(&text[4..19]);
let dog = Highlight(&text[35..43]);
// erase(text);
println!("{fox:?}");
println!("{dog:?}");
}
// case 2
// 返回值的生命周期与它真正依赖的数据源(即 points切片)的生命周期关联起来。
fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
// ... 函数体 ...
}
// case 3
fn main() {
let r;
{
let x = 5;
r = &x; // 错误:`x` 的生命周期不够长
}
println!("r: {}", r);
}
4.15. 文档测试
///注释中的代码块会自动被视为 Rust 代码。- 代码会作为
cargo test的一部分进行编译和执行。
#![allow(unused)]
fn main() {
/// Shortens a string to the given length.
///
/// ```
/// # use playground::shorten_string;
/// assert_eq!(shorten_string("Hello World", 5), "Hello");
/// assert_eq!(shorten_string("Hello World", 20), "Hello World");
/// ```
pub fn shorten_string(s: &str, length: usize) -> &str {
&s[..std::cmp::min(length, s.len())]
}
}
4.16. 尝试运算符
match some_expression {
Ok(value) => value,
Err(err) => return Err(err),
}
等同于
some_expression?
比如:
let username_file_result = fs::File::open(path);
let mut username_file = match username_file_result {
Ok(file) => file,
Err(err) => return Err(err),
};
let mut username_file = fs::File::open(path)?; // 使用 ? 操作符简化文件打开的错误处理
4.17. anyhow和thiserror宏
anyhow的设计初衷是让应用程序中的错误处理变得无比简单。它提供了一个通用的错误类型 anyhow::Error,使得你无需在每个函数签名中声明具体的错误类型。可以无缝使用 ?来传播错误,anyhow会自动进行类型转换 。
use anyhow::{Context, Result};
fn read_user_data(user_id: u32) -> Result<String> {
let path = format!("data/{}.json", user_id);
// 使用 ? 传播错误,并使用 .context() 添加上下文
let data = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read user data file for user {}", user_id))?;
// 解析JSON,同样使用 ? 传播错误(例如来自 serde_json 的错误)
let value: serde_json::Value = serde_json::from_str(&data)
.context("Failed to parse user data as JSON")?;
Ok(value.to_string())
}
fn main() -> Result<()> {
let data = read_user_data(42)?;
println!("User data: {}", data);
Ok(())
}
thiserror的核心价值在于让你能轻松地定义结构清晰、信息丰富的自定义错误类型,特别适合在库开发中使用,因为它为库的使用者提供了精确的错误信息,方便他们进行不同的处理。使用 #[error("...")]为每个错误变体定义人类可读的错误信息,并支持字符串插值
#[derive(Debug, Error)]
enum ParserError {
#[error("Tokenizer error: {0}")]
TokenizerError(#[from] TokenizerError),
#[error("Unexpected end of input")]
UnexpectedEOF,
#[error("Unexpected token {0:?}")]
UnexpectedToken(Token),
#[error("Invalid number")]
InvalidNumber(#[from] std::num::ParseIntError),
}
4.18. 线程thread
thread::spawn的基本使用非常简单:你传递一个闭包(closure)给它,闭包中的代码将在新线程中运行,并且不阻塞主线程
use std::thread;
use std::time::Duration;
fn main() {
// 创建一个新线程
let handle = thread::spawn(|| {
for i in 1..5 {
println!("子线程: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
// 主线程继续执行自己的任务
for i in 1..3 {
println!("主线程: {}", i);
thread::sleep(Duration::from_millis(1));
}
// 等待新线程执行完毕
handle.join().unwrap();
}
在线程闭包中如果需要使用外部变量,常常需要配合 move关键字来转移变量的所有权。这是因为新线程的生命周期可能长于创建它的函数,Rust 的所有权系统可以防止悬垂引用等内存安全问题
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// 使用 move 将 data 的所有权转移到新线程的闭包中
let handle = thread::spawn(move || {
// 现在 data 属于这个新线程
for num in data {
println!("处理数字: {}", num);
}
// data 在这里被丢弃
});
// 在主线程中不能再使用 data,因为所有权已经转移
// println!("{:?}", data); // 这行代码会导致编译错误
handle.join().unwrap(); 输出:线程返回的结果
}
线程间的通信使用channel
use std::thread;
use std::sync::mpsc; // mpsc: Multiple Producer, Single Consumer
fn main() {
// 创建一个通道
let (tx, rx) = mpsc::channel();
// 在新线程中发送消息
thread::spawn(move || {
let message = String::from("你好,主线程!");
tx.send(message).unwrap(); // 发送消息
// 注意:发送后,message 的所有权也转移了
});
// 在主线程中接收消息
// recv() 会阻塞,直到收到消息
let received = rx.recv().unwrap();
println!("收到: {}", received); // 输出:收到: 你好,主线程!
}
4.19. Send和Sync trait
Send和 Sync是 Rust 语言中保证线程安全的核心标记 trait(marker trait),它们在编译期通过静态检查来防止数据竞争。简单来说,它们定义了数据能否安全地在线程间"移动"或"共享"。
Send 关乎所有权移动。它回答"这个数据能安全地交给另一个线程吗?"
Sync 关乎引用共享。它回答"这个数据的只读引用能安全地同时给多个线程使用吗?"
Send标记一个类型可以安全地将其所有权从一个线程移动到另一个线程。绝大多数 Rust 标准库类型都实现了 Send。
use std::thread;
use std::rc::Rc; // Rc<T> 没有实现 Send
fn main() {
let data = vec![1, 2, 3, 4, 5]; // Vec<T> 实现了 Send
// let data = Rc::new(42); // Rc<T> 没有实现 Send,因此会报错
// 这是因为 Rc的引用计数更新不是原子操作,
// 线程同时修改会导致计数错误。此时应使用线程安全的 Arc(原子引用计数)
// 使用 move 关键字将 data 的所有权转移到新线程
let handle = thread::spawn(move || {
println!("Data in new thread: {:?}", data);
// 在这里,data 属于这个新线程
});
handle.join().unwrap();
// 此后,主线程不能再使用 data,因为所有权已经转移
}
Sync标记一个类型可以安全地被多个线程同时共享其不可变引用 (&T)。更准确地说,T: Sync意味着 &T: Send,即你可以安全地将一个不可变引用发送到另一个线程使用。
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(42); // Arc<T> 实现了 Sync(当 T: Send + Sync 时)
let mut handles = vec![];
for i in 0..5 {
let data_clone = Arc::clone(&data); // 克隆 Arc,增加引用计数
let handle = thread::spawn(move || {
// 多个线程可以同时安全地读取 data_clone 指向的数据
println!("Thread {}: {}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
use std::thread;
use std::cell::RefCell; // RefCell<T> 没有实现 Sync
fn main() {
let data = RefCell::new(42);
let handle = thread::spawn(move || { // 这里会触发编译错误!
println!("{}", data.borrow());
});
handle.join().unwrap();
}
4.20. Mutex
在 Rust 的并发编程中,Mutex(互斥锁)是保护共享数据、防止数据竞争的核心工具之一。
Mutex<T>本身是一个智能指针,对其调用 lock方法会返回一个 MutexGuard<T>,该守卫实现了 Deref和 Droptrait,方便直接操作数据并自动释放锁。
use std::sync::Mutex;
fn main() {
// 创建一个保护 i32 类型数据的 Mutex,初始值为 5
let mux = Mutex::new(5);
{
// 获取锁,返回一个 MutexGuard
// lock() 会阻塞当前线程,直到获取到锁
// 使用 unwrap() 在成功获取锁后解出守卫,如果锁中毒(poisoned)则会 panic
let mut num = mux.lock().unwrap();
*num = 10; // 通过 Deref 解引用修改数据
println!("Value inside mutex: {}", *num);
// 作用域结束,MutexGuard 被 drop,锁自动释放
}
// 再次获取锁,确认值已被修改
println!("Now mux = {:?}", mux); // 输出: Mutex { data: 10, poisoned: false, .. }
}
要在多个线程间共享同一个 Mutex,需要使用 Arc(原子引用计数智能指针),因为它实现了 Send和 Sync,可以安全地将所有权转移到线程中。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 使用 Arc 包装 Mutex,以实现多线程间的共享所有权
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// 克隆 Arc指针,增加引用计数,让另一个线程也获得指向同一个 Mutex的权限。
// 绝不是复制数据或锁本身。
// 克隆 Arc 的引用计数,每个线程持有其一份克隆
// 它让多个线程都能“拥有”访问同一份数据的权利。
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || { // 将克隆的所有权移入线程
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// 等待所有线程结束
for handle in handles {
handle.join().unwrap();
}
// 打印最终结果
println!("Final counter: {}", *counter.lock().unwrap()); // 输出: Final counter: 10
}
有一些注意事项:
- 锁中毒(Poisoning) :如果一个线程在持有
Mutex锁时发生 panic,Mutex会标记自己为"中毒"状态。后续尝试lock时会返回Err。你可以选择处理这个错误或直接unwrap让当前线程也 panic,这取决于你的应用场景 - 死锁(Deadlock):如果两个或多个线程各自持有一些锁,并同时等待对方释放锁,就可能发生死锁,所有相关线程都会被无限期阻塞。避免死锁需要仔细设计锁的获取顺序
- 性能考量 :
Mutex的锁操作有一定开销。应尽量缩小锁的持有时间,即获取锁后尽快完成操作并释放锁,避免在持有锁的情况下执行耗时操作或等待I/O
4.21. 异步Tokio
Tokio 是 Rust 生态中最主流的异步运行时 ,它让你能够编写高性能、高并发的网络应用程序。简单来说,Tokio 为 Rust 的 async/await语法提供了底层的执行引擎,让你能用同步代码的书写风格,获得异步非阻塞的性能优势。
|--------|------------------------|-----------------------------|
| 核心组件 | 主要作用 | 通俗理解 |
| 异步 I/O | 提供非阻塞的网络(TCP/UDP)和文件操作 | 让程序在等待数据时不去"干等",而是去处理其他任务 |
| 任务调度器 | 管理并执行大量的异步任务(Future) | 一个高效的"任务指挥官",确保 CPU 核心被充分利用 |
| 定时器 | 处理延迟、超时和周期性任务 | 一个精准的"异步闹钟" |
| 同步原语 | 为异步环境提供互斥锁、信道等工具 | 让多个异步任务可以安全地共享和传递数据 |
// 1. 顺序执行
use tokio::time::{sleep, Duration};
// 定义一个异步函数
async fn say_after(delay: u64, msg: &str) {
sleep(Duration::from_secs(delay)).await; // 使用 .await 等待异步操作,不会阻塞线程
println!("{}", msg);
}
#[tokio::main] // 这个宏将 main 函数转换为异步函数并启动运行时
async fn main() {
// 使用 `.await` 按顺序执行
say_after(1, "Hello").await;
say_after(2, "World").await;
}
// 2. 并发执行
#[tokio::main]
async fn main() {
// 使用 `spawn` 并发执行两个任务
let task1 = tokio::spawn(async {
say_after(2, "Task 1 completed").await;
});
let task2 = tokio::spawn(async {
say_after(1, "Task 2 completed").await; // 这个任务先完成
});
// 等待两个任务都完成
let _ = tokio::join!(task1, task2);
println!("All tasks done!");
}
// 3.异步环境下的数据共享
use tokio::sync::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
// 创建一个受保护的可变计数器
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
// 创建10个并发任务,每个任务对计数器加1
let handle = tokio::spawn(async move {
let mut num = counter.lock().await; // 异步获取锁,不会阻塞线程
*num += 1;
});
handles.push(handle);
}
// 等待所有任务完成
for handle in handles {
handle.await.unwrap();
}
// 打印最终结果
println!("Final count: {}", *counter.lock().await); // 输出:Final count: 10
}
与标准库的同步 std::sync::Mutex不同,tokio::sync::Mutex的 lock方法是异步的。在等待锁时,它会让出线程控制权,允许运行时执行其他任务,从而极大提升程序的并发吞吐量,特别适合存在锁竞争但锁持有时间很短的场景。
5. 小巧的Rust开源项目-Nping源码阅读
Nping 是一个基于 Rust 开发的终端可视化 Ping 工具, 支持多地址并发 Ping, 可视化图表展示, 数据实时更新等特性。

让我们看看他是如何实现的,核心架构如下:
- 主流程 main.rs
命令行参数解析,使用了clap库

#[derive(Parser, Debug)]
#[command(
version = "v0.5.0",
author = "hanshuaikang<https://github.com/hanshuaikang>",
about = "🏎 Nping mean NB Ping, A Ping Tool in Rust with Real-Time Data and Visualizations"
)]
struct Args {
/// Target IP address or hostname to ping
#[arg(help = "target IP address or hostname to ping", required = true)]
target: Vec<String>,
/// Number of pings to send
#[arg(short, long, default_value_t = 65535, help = "Number of pings to send")]
count: usize,
/// Interval in seconds between pings
#[arg(short, long, default_value_t = 0, help = "Interval in seconds between pings")]
interval: i32,
#[clap(long = "force_ipv6", default_value_t = false, short = '6', help = "Force using IPv6")]
pub force_ipv6: bool,
// ... 其他参数
}
要ping哪些ip,首先先去重,然后根据ip数量计算工作线程:
// 去重但保持原有顺序
let mut seen = HashSet::new();
let targets: Vec<String> = args.target.into_iter()
.filter(|item| seen.insert(item.clone()))
.collect();
// 根据 IP 数量计算工作线程数
let ip_count = if targets.len() == 1 && args.multiple > 0 {
args.multiple as usize
} else {
targets.len()
};
let worker_threads = (ip_count + 1).max(1);
使用tokio库,创建多线程异步执行run_app这个函数:
// 创建具有特定工作线程数的 tokio 运行时
let rt = Builder::new_multi_thread()
.worker_threads(worker_threads)
.enable_all()
.build()?;
// 运行异步应用
let res = rt.block_on(run_app(targets, args.count, args.interval, running.clone(), args.force_ipv6, args.multiple, args.view_type, args.output));
多线程并发执行的核心在run_app里面
for (i, ip) in ips.iter().enumerate() {
let ip = ip.clone();
let running = running.clone();
let errs = errs.clone();
let task = task::spawn({ // 这里创建异步任务
// 每个 IP 地址的任务,去执行ping命令,使用的是pinger库
async move {
send_ping(addr, ip, errs.clone(), count, interval, running.clone(), ping_event_tx).await.unwrap();
}
});
tasks.push(task)
}
使用channel将ping结果发给数据处理模块,然后数据处理模块再发给ui模块
// ping event channel (network -> data processor)
let (ping_event_tx, ping_event_rx) = mpsc::sync_channel::<PingEvent>(0);
// ui data channel (data processor -> ui)
let (ui_data_tx, ui_data_rx) = mpsc::sync_channel::<IpData>(0);
let ping_event_tx = Arc::new(ping_event_tx);
启动两个线程任务,一个数据处理,一个UI
start_data_processor(
ping_event_rx,
ui_data_tx,
targets_for_processor,
view_type.clone(),
running.clone(),
);
let ui_task = task::spawn(async move {
let mut guard = terminal_guard_for_ui.lock().unwrap();
draw::draw_interface_with_updates(
&mut guard.terminal.as_mut().unwrap(),
&view_type_for_ui,
&ip_data_for_ui,
ui_data_rx,
running_for_ui,
errs_for_ui,
output_file,
).ok();
});
所以线程都可以通过running: Arc<Mutex<bool>> 进行控制退出
一般是ctrl + c 会使 running =false
- 网络模块 network.rs
send_ping的具体实现,利用ping库:
let stream = ping(options)?;
match stream.recv() {
Ok(result) => {
match result {
... ...
// result枚举如下
pub enum PingResult {
Pong(Duration, String),
Timeout(String),
Unknown(String),
PingExited(ExitStatus, String),
}
// 发给数据处理
let _ = tx.send(PingResult::PingExited(result.status, decoded_stderr));
- 数据处理模块 data_processor.rs
pub fn process_event(&mut self, event: PingEvent) -> Option<IpData> {
match event {
PingEvent::Success { addr, ip, rtt, .. } => {
let key = format!("{}_{}", addr, ip);
if let Some(data) = self.data_map.get_mut(&key) {
Self::update_success_stats(data, rtt, self.point_num);
Some(data.clone())
} else {
None
}
},
PingEvent::Timeout { addr, ip, .. } => {
let key = format!("{}_{}", addr, ip);
if let Some(data) = self.data_map.get_mut(&key) {
Self::update_timeout_stats(data, self.point_num);
Some(data.clone())
} else {
None
}
},
}
}
更新这个解构
pub struct IpData {
pub(crate) addr: String,
pub(crate) ip: String,
pub(crate) rtts: VecDeque<f64>,
pub(crate) last_attr: f64,
pub(crate) min_rtt: f64,
pub(crate) max_rtt: f64,
pub(crate) timeout: usize,
pub(crate) received: usize,
pub(crate) pop_count: usize,
}
- ui模块 ui目录
所有视图都使用ratatui库构建终端用户界面:
- 使用Layout进行布局管理
- 使用Constraint定义各部分大小比例
- 使用不同颜色表示不同状态(绿色正常、黄色警告、红色错误)
- 支持实时更新和键盘交互(q/Esc/Ctrl+C退出)
以graph为例:

这是默认视图,主要特点:
- 每行显示最多5个目标的详细信息
- 每个目标显示:
- 目标地址和IP
- 最后、平均、最大、最小RTT延迟
- 抖动(Jitter)和丢包率(Loss)
- 实时延迟图表(使用线条图)
- 最近5条记录
- 底部显示错误信息
// 函数接受帧对象、IP 数据和错误信息作为参数
// 计算需要显示的行数(每行最多显示5个目标)
// 初始化布局块容器
pub fn draw_graph_view<B: Backend>(
f: &mut Frame,
ip_data: &[IpData],
errs: &[String]) {
let size = f.area();
let rows = (ip_data.len() as f64 / 5.0).ceil() as usize;
let mut chunks = Vec::new();
// 这部分循环处理每一行,每行最多显示5个目标的监控信息。
for (row, vertical_chunk) in vertical_chunks.iter().enumerate().take(rows) {
let start = row * 5;
let end = (start + 5).min(ip_data.len());
let row_data = &ip_data[start..end];
// 水平布局约束
let horizontal_constraints: Vec<Constraint> = if row_data.len() == 5 {
row_data.iter().map(|_| Constraint::Percentage(20)).collect()
} else {
// when the number of targets is less than 5, we need to adjust the size of each target
let mut size = 100;
if ip_data.len() > 5 {
size = row_data.len() * 20;
}
row_data.iter().map(|_| Constraint::Percentage(size as u16 / row_data.len() as u16)).collect()
};
// 单目标渲染
// 计算延迟
et loss_pkg = if data.timeout > 0 {
(data.timeout as f64 / (data.received as f64 + data.timeout as f64)) * 100.0
} else {
0.0
};
let loss_pkg_color = if loss_pkg > 50.0 {
Color::Red
} else if loss_pkg > 0.0 {
Color::Yellow
} else {
Color::Green
};
// 计算其他指标
let base_metric_text = Line::from(vec![
Span::styled("Last: ", Style::default()),
Span::styled(
if data.last_attr == 0.0 {
"< 0.01ms".to_string()
} else if data.last_attr == -1.0 {
"0.0ms".to_string()
} else {
format!("{:?}ms", data.last_attr)
},
Style::default().fg(Color::Green)
),
// ... 其他指标
]);
6. Reference
https://www.bookstack.cn/read/comprehensive-rust-202412-zh/c01514b4d45220e0.md