纯 CPU ------ 并行质数寻找器 (Parallel Prime Finder)
目标: 找出 1 到 10,000,000 (一千万) 之间的所有质数。 核心逻辑: 这是一个纯计算任务。判断一个数是不是质数,需要大量的除法运算,CPU 必须一直在转。 工具: 这里我们不用 Tokio 。对于纯 CPU 任务,Rust 社区最常用的库是 Rayon 。它能把你的 for 循环自动变成并行的。
Cargo.toml:
Ini, TOML
ini
[dependencies]
rayon = "1.8"
Rust
rust
use rayon::prelude::*; // 引入 Rayon 的并行迭代器功能
// 一个非常笨重、耗费 CPU 的函数:判断 n 是否为质数
fn is_prime(n: u32) -> bool {
if n <= 1 { return false; }
// 笨办法:从 2 遍历到 sqrt(n),疯狂做除法
let limit = (n as f64).sqrt() as u32;
for i in 2..=limit {
if n % i == 0 {
return false;
}
}
true
}
fn main() {
let limit = 10_000_000;
println!("--- 开始寻找 1 到 {} 之间的质数 ---", limit);
let start_time = std::time::Instant::now();
// 方式 A: 单线程写法 (对比组)
// 只要把下面的 par_iter() 改成 iter() 就是单线程
// let count = (1..limit).filter(|&n| is_prime(n)).count();
// 方式 B: 并行写法 (Rayon)
// par_iter() 会自动把 1..limit 切成好几块,分发给所有的 CPU 核
let count = (1..limit)
.into_par_iter() // <--- 魔法在这里!变成并行迭代器
.filter(|&n| is_prime(n)) // 这会在多个核上同时跑
.count();
println!("找到 {} 个质数", count);
println!("--- 计算结束,耗时: {:?} ---", start_time.elapsed());
}
练习重点:
-
观察 CPU: 运行这个程序时,打开你的任务管理器/活动监视器。你会看到 CPU 占用率瞬间飙升到 100%(所有核都被吃满了)。
-
对比试验: 试着把
.into_par_iter()改回普通的.into_iter()。- 并行版: 可能只要 1-2 秒(取决于你的核数)。
- 单线程版: 可能需要 5-8 秒。


磁盘 I/O 是实打实能看到硬盘读写飙升的。
我们来做一个 "极速文件备份引擎" (Async File Replicator) 。
核心目标
- 场景: 模拟你有一堆巨大的日志文件(比如 50 个 100MB 的文件),需要从 A 文件夹备份到 B 文件夹。
- 挑战: 也就是通常说的"拷贝文件"。如果是单线程,是一个个拷;我们要用
tokio并发拷,看能不能把你的 SSD 吞吐量打满。 - 观测: 教你如何用系统工具监控这个过程。
第一步:先学会"看" I/O (监控工具)
在跑代码之前,先把监控面板打开,这样代码一跑,你就能看到波形图跳起来。
1. Windows 用户
- 基础版: 打开 任务管理器 (Task Manager) -> 性能 (Performance) -> 点击 磁盘 (Disk) 。关注 "活动时间 (Active time)" 和 "磁盘传输速率 (Transfer Rate)" 。
- 进阶版 (推荐): Win + R 输入
resmon打开 资源监视器 。点击 磁盘 选项卡。这里能看到具体是哪个exe在读写,以及读写了哪个文件。
2. macOS 用户
- 打开 活动监视器 (Activity Monitor) (Cmd + Space 搜索 Activity Monitor)。
- 点击底部的 "磁盘 (Disk)" 标签页。
- 关注底部的 "数据读取/秒 (Data read/sec)" 和 "数据写入/秒 (Data written/sec)" 。你会看到这两个数字瞬间飙升。
3. Linux 用户
- 在终端输入
iotop(需要 sudo)。你会看到实时的磁盘 I/O 排行榜。
第二步:项目代码 ------ 极速备份引擎
这个项目分为两部分:
- 生成器: 快速生成一堆垃圾文件用来测试。
- 备份器: 使用 Tokio 并发复制。
Cargo.toml:
Ini, TOML
ini
[dependencies]
tokio = { version = "1", features = ["full"] }
rand = "0.8" # 用来生成随机文件名或内容(可选,这里其实只用0填充也行)
Rust
rust
use std::path::Path;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::time::Instant;
use std::sync::Arc;
use tokio::sync::Semaphore;
// 设定测试规模
const FILE_COUNT: usize = 20; // 文件数量
const FILE_SIZE_MB: usize = 50; // 每个文件 50MB (总共 1GB)
const SOURCE_DIR: &str = "./source_data";
const TARGET_DIR: &str = "./backup_data";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// --- 准备工作 ---
println!("--- 阶段 1: 准备测试数据 ---");
prepare_directories().await?;
generate_dummy_files().await?;
println!("\n请现在打开你的【活动监视器/任务管理器/iotop】... 3秒后开始疯狂拷贝");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
// --- 核心工作: 异步并发拷贝 ---
println!("--- 阶段 2: 开始极速备份 ---");
let start = Instant::now();
// 限制并发数:虽然是 I/O,但如果不限制,瞬间打开几千个文件句柄会报错
// 同时也为了观察持续的 I/O 压力,而不是一瞬间结束
let semaphore = Arc::new(Semaphore::new(5));
let mut tasks = vec![];
// 读取源目录
let mut entries = fs::read_dir(SOURCE_DIR).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_file() {
let sem_clone = semaphore.clone();
// 开启一个异步任务去拷贝这个文件
let task = tokio::spawn(async move {
// 获取令牌
let _permit = sem_clone.acquire().await.unwrap();
let file_name = path.file_name().unwrap().to_str().unwrap();
let target_path = Path::new(TARGET_DIR).join(file_name);
// ⚠️ 核心 I/O 操作:tokio::fs::copy
// 这行代码底层会利用 OS 的异步文件接口
match fs::copy(&path, &target_path).await {
Ok(bytes) => {
println!("✅ 已备份: {} ({:.2} MB)", file_name, bytes as f64 / 1024.0 / 1024.0);
}
Err(e) => eprintln!("❌ 失败 {}: {}", file_name, e),
}
// 令牌自动释放
});
tasks.push(task);
}
}
// 等待所有拷贝任务完成
for task in tasks {
let _ = task.await;
}
let duration = start.elapsed();
let total_size_mb = (FILE_COUNT * FILE_SIZE_MB) as f64;
let speed = total_size_mb / duration.as_secs_f64();
println!("\n--- 备份完成 ---");
println!("耗时: {:.2?}", duration);
println!("平均吞吐量: {:.2} MB/s", speed);
// 清理现场(可选)
// fs::remove_dir_all(SOURCE_DIR).await?;
// fs::remove_dir_all(TARGET_DIR).await?;
Ok(())
}
// 辅助函数:生成垃圾文件
async fn generate_dummy_files() -> std::io::Result<()> {
// 创建一个 全是 0 的 50MB 缓冲区
let buffer = vec![0u8; FILE_SIZE_MB * 1024 * 1024];
for i in 0..FILE_COUNT {
let file_path = Path::new(SOURCE_DIR).join(format!("log_{}.dat", i));
if !file_path.exists() {
println!("生成测试文件: log_{}.dat ...", i);
let mut file = fs::File::create(file_path).await?;
file.write_all(&buffer).await?;
}
}
Ok(())
}
// 辅助函数:创建目录
async fn prepare_directories() -> std::io::Result<()> {
if !Path::new(SOURCE_DIR).exists() {
fs::create_dir(SOURCE_DIR).await?;
}
if Path::new(TARGET_DIR).exists() {
fs::remove_dir_all(TARGET_DIR).await?;
}
fs::create_dir(TARGET_DIR).await?;
Ok(())
}

-
文件系统的 Async I/O:
tokio::fs模块里的 API (read_dir,copy,File::create) 和标准库std::fs长得几乎一样,但它们全是 非阻塞 的。- 当你调用
fs::copy时,如果硬盘忙不过来,Tokio 线程会去处理别的任务,而不是干等着。
-
观察系统瓶颈:
- 当你运行这个程序时,盯着你的任务管理器。
- 如果是机械硬盘 (HDD): 你的吞吐量可能只有 100MB/s,而且你会听到硬盘疯狂寻道的声音(磁头在源文件和目标文件之间来回跳)。
- 如果是固态硬盘 (NVMe SSD): 你可能会看到 1GB/s - 2GB/s 的惊人速度,瞬间跑完。
-
并发控制 (Semaphore) 的重要性:
- 试着把
Semaphore::new(5)改成Semaphore::new(100)。 - 如果在机械硬盘上,速度反而会变慢。因为磁头来回跳跃(随机读写)的开销太大了,不如排队顺序读写(顺序 I/O)快。
- 这就是为什么即便用了异步,我们依然需要限流。
- 试着把