Rust 的CPU和IO操作

纯 CPU ------ 并行质数寻找器 (Parallel Prime Finder)

目标: 找出 1 到 10,000,000 (一千万) 之间的所有质数。 核心逻辑: 这是一个纯计算任务。判断一个数是不是质数,需要大量的除法运算,CPU 必须一直在转。 工具: 这里我们不用 Tokio 。对于纯 CPU 任务,Rust 社区最常用的库是 Rayon 。它能把你的 for 循环自动变成并行的。

Cargo.toml:

Ini, TOML

ini 复制代码
[dependencies]
rayon = "1.8" 

main.rs:

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());
}

练习重点:

  1. 观察 CPU: 运行这个程序时,打开你的任务管理器/活动监视器。你会看到 CPU 占用率瞬间飙升到 100%(所有核都被吃满了)。

  2. 对比试验: 试着把 .into_par_iter() 改回普通的 .into_iter()

    • 并行版: 可能只要 1-2 秒(取决于你的核数)。
    • 单线程版: 可能需要 5-8 秒。

磁盘 I/O 是实打实能看到硬盘读写飙升的。

我们来做一个 "极速文件备份引擎" (Async File Replicator)

核心目标

  1. 场景: 模拟你有一堆巨大的日志文件(比如 50 个 100MB 的文件),需要从 A 文件夹备份到 B 文件夹。
  2. 挑战: 也就是通常说的"拷贝文件"。如果是单线程,是一个个拷;我们要用 tokio 并发拷,看能不能把你的 SSD 吞吐量打满。
  3. 观测: 教你如何用系统工具监控这个过程。

第一步:先学会"看" 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 排行榜。

第二步:项目代码 ------ 极速备份引擎

这个项目分为两部分:

  1. 生成器: 快速生成一堆垃圾文件用来测试。
  2. 备份器: 使用 Tokio 并发复制。

Cargo.toml:

Ini, TOML

ini 复制代码
[dependencies]
tokio = { version = "1", features = ["full"] }
rand = "0.8" # 用来生成随机文件名或内容(可选,这里其实只用0填充也行)

main.rs:

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(())
}
  1. 文件系统的 Async I/O:

    • tokio::fs 模块里的 API (read_dir, copy, File::create) 和标准库 std::fs 长得几乎一样,但它们全是 非阻塞 的。
    • 当你调用 fs::copy 时,如果硬盘忙不过来,Tokio 线程会去处理别的任务,而不是干等着。
  2. 观察系统瓶颈:

    • 当你运行这个程序时,盯着你的任务管理器。
    • 如果是机械硬盘 (HDD): 你的吞吐量可能只有 100MB/s,而且你会听到硬盘疯狂寻道的声音(磁头在源文件和目标文件之间来回跳)。
    • 如果是固态硬盘 (NVMe SSD): 你可能会看到 1GB/s - 2GB/s 的惊人速度,瞬间跑完。
  3. 并发控制 (Semaphore) 的重要性:

    • 试着把 Semaphore::new(5) 改成 Semaphore::new(100)
    • 如果在机械硬盘上,速度反而会变慢。因为磁头来回跳跃(随机读写)的开销太大了,不如排队顺序读写(顺序 I/O)快。
    • 这就是为什么即便用了异步,我们依然需要限流
相关推荐
Lucky_Turtle2 小时前
【Springboot】解决PageHelper在实体转Vo下出现total数据问题
java·spring boot·后端
無量2 小时前
AI工程化实践指南:从入门到落地
后端·ai编程
golang学习记2 小时前
Jetbrains 这个知名软件十年了!
后端
老华带你飞2 小时前
志愿者服务管理|基于springboot 志愿者服务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
知其然亦知其所以然2 小时前
程序员的最强外挂:用 Spring AI 解锁智谱 AI 画图能力
后端·spring·程序员
汤姆yu2 小时前
基于springboot的宠物服务管理系统
java·spring boot·后端
Charlie_Byte3 小时前
用 MurmurHash + Base62 生成短链接
java·后端
老华带你飞3 小时前
学生请假管理|基于springboot 学生请假管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端·spring