Learn
Rust/30-project-concurrent

项目实战:并发文件处理

收官之战:把前面学到的线程、消息传递、共享状态、迭代器串起来,写一个并发统计目录下多个文件行数的工具。

1. 设计思路

  • 主线程把多个文件路径通过 channel 发给工作线程池。
  • 每个工作线程独立读文件、统计行数,把 (路径, 行数) 通过另一 channel 发回。
  • 主线程收集结果汇总。这正是「用通信共享内存」的典范。

2. 完整实现

use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use std::fs;
 
fn main() {
    let paths = vec![
        "a.txt".to_string(),
        "b.txt".to_string(),
        "c.txt".to_string(),
    ];
 
    let (tx_task, rx_task) = mpsc::channel();
    let (tx_result, rx_result) = mpsc::channel();
 
    // 启动 3 个工作线程
    let workers = 3;
    for _ in 0..workers {
        let rx_task = rx_task.clone();
        let tx_result = tx_result.clone();
        thread::spawn(move || {
            for path in rx_task {
                let lines = fs::read_to_string(&path)
                    .map(|c| c.lines().count())
                    .unwrap_or(0);
                tx_result.send((path, lines)).unwrap();
            }
        });
    }
    drop(tx_result);   // 关闭多余发送端,否则接收端不会结束
 
    // 分发任务
    for p in &paths {
        tx_task.send(p.clone()).unwrap();
    }
    drop(tx_task);
 
    // 汇总
    let mut total = 0;
    for (path, lines) in rx_result {
        println!("{}: {} 行", path, lines);
        total += lines;
    }
    println!("合计: {} 行", total);
}
ℹ️channel 自动转移所有权

工作线程 move 进 rx_task,文件内容在另一线程读、结果通过 tx_result 发回。全程无共享可变内存,数据竞争无从发生。

3. 关键点解析

  • rx_task.clone():多消费者需要每个线程一份接收端。
  • drop(tx_result):所有发送端关闭后,for ... in rx_result 才会自然结束(否则一直阻塞)。
  • unwrap_or(0):文件读不到时计 0 行,程序更健壮。
⚠️忘记 drop 发送端会死等

若保留多余的 tx_result/tx_task 发送端不 drop,接收端会永远等待更多消息,主线程卡死。这是 channel 并发最常见的坑。

4. 用线程池替代手写

生产环境可用 rayon 一行并行化:

use rayon::prelude::*;
let total: usize = paths.par_iter()
    .map(|p| fs::read_to_string(p).map(|c| c.lines().count()).unwrap_or(0))
    .sum();
💡rayon 让并行变简单

rayon 提供 par_iter(),把普通迭代器变成并行迭代器,自动调度线程池,几乎零改造即得并发加速。

5. 进阶方向

完成本项目后,你已经具备独立开发 Rust 应用的能力。可继续探索:异步 Web(actix/tokio)、数据库(sqlx/diesel)、命令行生态(clap + anyhow)、以及嵌入式/WASM 等方向。

🎯练习

把上面工具改成:用 Arc<Mutex<HashMap<String, usize>>> 让工作线程直接写共享结果表,主线程 join 所有 worker 后再打印;对比 channel 版,体会两种共享方式的差异。

小结

  • channel 实现「通信式」并发,天然无数据竞争
  • 多消费线程需克隆接收端;结束要 drop 发送端
  • 共享结果也可用 Arc<Mutex<HashMap>>,与 channel 对照理解
  • 综合运用:线程 + 所有权 + 迭代器,已具备实战能力
  • 恭喜完成全部 30 章 Rust 课程 🎉