第九篇:《并发编程:线程、Channel 与共享状态》

" fearless concurrency"------无畏并发,是 Rust 官方文档中对并发能力的描述。在 C/C++ 中,写并发代码需要极度谨慎,一个疏忽就可能导致数据竞争、死锁或难以复现的 bug。Rust 通过类型系统在编译期消灭了数据竞争,让并发编程变得"无畏"。本文从线程的创建与生命周期讲起,系统讲解消息传递(Channel)和共享状态(Arc<Mutex>)两种并发范式,深入剖析 Send 和 Sync trait 如何保证编译期并发安全,并通过一个并发 Web 爬虫实战帮你掌握 Rust 并发编程的核心技能。

一、线程基础

1.1 创建线程

Rust 使用 std::thread::spawn 创建线程,参数是一个闭包:

rust 复制代码
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(500));
        }
    });

    for i in 1..=3 {
        println!("主线程: {}", i);
        thread::sleep(Duration::from_millis(800));
    }

    // 等待子线程结束
    handle.join().unwrap();
    println!("所有线程结束");
}

关键点:

spawn 返回 JoinHandle,通过 join() 等待线程结束。

join() 返回 Result<T, Box<dyn Any + Send>>,线程 panic 时返回 Err。

主线程结束时,所有子线程被强制终止------join 确保子线程执行完毕。

1.2 move 闭包:转移所有权到线程

线程的生命周期可能超过创建它的作用域,因此闭包必须获取数据的所有权:

rust 复制代码
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // ❌ 编译错误:闭包可能比 data 活得更久
    // let handle = thread::spawn(|| {
    //     println!("{:?}", data);
    // });

    // ✅ 使用 move 转移所有权
    let handle = thread::spawn(move || {
        println!("线程中的数据: {:?}", data);
    });

    // data 已被移动,不再可用
    handle.join().unwrap();
}

move 的本质:将闭包捕获的变量所有权转移到闭包中,从而转移到线程中。

1.3 线程 panic 处理

rust 复制代码
use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        panic!("子线程崩溃了!");
    });

    match handle.join() {
        Ok(_) => println!("线程正常结束"),
        Err(e) => {
            let msg = e.downcast_ref::<&str>().unwrap_or(&"未知错误");
            println!("线程 panic: {}", msg);
        }
    }
}

二、消息传递:Channel

Rust 的并发哲学之一是:不要通过共享内存来通信,而要通过通信来共享内存。Channel 是实现这一理念的核心工具。

2.1 创建 Channel

rust 复制代码
use std::sync::mpsc;
use std::thread;

fn main() {
    // mpsc = Multiple Producer, Single Consumer
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        tx.send(String::from("hello")).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("收到: {}", received);
}

mpsc 的含义:多个生产者(Sender),单个消费者(Receiver)。

2.2 发送多个消息

rust 复制代码
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let messages = vec!["消息1", "消息2", "消息3"];
        for msg in messages {
            tx.send(msg).unwrap();
            thread::sleep(Duration::from_millis(500));
        }
    });

    // 遍历接收所有消息
    for received in rx {
        println!("收到: {}", received);
    }
    // 当 tx 被 drop 后,循环自动结束
}

for received in rx:当所有 Sender 都被 drop 后,Receiver 会收到 Err,循环自动结束。

2.3 多个生产者

rust 复制代码
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    for i in 0..3 {
        let tx = tx.clone();   // 克隆 Sender
        thread::spawn(move || {
            tx.send(format!("来自线程 {}", i)).unwrap();
        });
    }

    drop(tx);   // 释放原始 Sender

    for received in rx {
        println!("收到: {}", received);
    }
}

⚠️ 注意:必须 drop 原始 tx,否则 Receiver 会一直等待。

2.4 同步 Channel:sync_channel

rust 复制代码
use std::sync::mpsc;
use std::thread;

fn main() {
    // 有界缓冲区大小为 2
    let (tx, rx) = mpsc::sync_channel(2);

    thread::spawn(move || {
        for i in 1..=5 {
            println!("发送: {}", i);
            tx.send(i).unwrap();   // 缓冲区满时阻塞
            println!("已发送: {}", i);
        }
    });

    thread::sleep(std::time::Duration::from_millis(100));

    for received in rx {
        println!("收到: {}", received);
        thread::sleep(std::time::Duration::from_millis(500));
    }
}

sync_channel 的价值:提供背压(Backpressure) ------当消费者处理不过来时,生产者会被阻塞,避免内存无限增长。

三、共享状态:Arc<Mutex>

3.1 为什么需要 Mutex?

多个线程共享数据时,需要确保同一时刻只有一个线程能修改数据:

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("最终计数: {}", *counter.lock().unwrap());   // 10
}

Arc<Mutex> 的解读:

Arc 让多个线程共享所有权。

Mutex 确保同一时刻只有一个线程能访问数据。

3.2 Mutex 的锁守卫

lock() 返回 MutexGuard,它实现了 Deref 和 Drop:

rust 复制代码
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }   // num 离开作用域,锁自动释放

    println!("m = {:?}", m);
}

锁守卫的生命周期就是锁的持有时间------离开作用域时自动解锁。这避免了忘记解锁导致的死锁。

3.3 避免死锁

死锁发生在两个线程互相等待对方持有的锁:

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let a = Arc::new(Mutex::new(1));
    let b = Arc::new(Mutex::new(2));

    let a1 = Arc::clone(&a);
    let b1 = Arc::clone(&b);
    let a2 = Arc::clone(&a);
    let b2 = Arc::clone(&b);

    let t1 = thread::spawn(move || {
        let _lock_a = a1.lock().unwrap();
        thread::sleep(std::time::Duration::from_millis(10));
        let _lock_b = b1.lock().unwrap();   // 等待 b
        println!("线程1 完成");
    });

    let t2 = thread::spawn(move || {
        let _lock_b = b2.lock().unwrap();
        thread::sleep(std::time::Duration::from_millis(10));
        let _lock_a = a2.lock().unwrap();   // 等待 a
        println!("线程2 完成");
    });

    t1.join().unwrap();
    t2.join().unwrap();
}

避免死锁的策略:

按固定的顺序获取锁(如总是先锁 a 再锁 b)。

使用 try_lock 避免无限等待。

尽量减少锁的持有时间。

四、Send 与 Sync:编译期并发安全

Rust 的并发安全基石是 Send 和 Sync 两个 marker trait:

4.1 Send 和 Sync 的自动推导

绝大多数类型自动实现 Send 和 Sync。只有少数类型例外:

rust 复制代码
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    // ❌ Rc 不是 Send,不能跨线程转移
    let rc = Rc::new(5);
    // thread::spawn(move || { println!("{}", rc); });

    // ❌ RefCell 不是 Sync,不能被多个线程引用
    let refcell = RefCell::new(5);
    // thread::spawn(move || { println!("{}", refcell.borrow()); });
}

为什么 Rc 不是 Send? 因为它的引用计数不是原子操作。如果两个线程同时克隆 Rc,可能导致计数错误,最终双重释放。

为什么 RefCell 不是 Sync? 因为它使用非原子的借用计数。两个线程同时 borrow_mut() 会导致数据竞争。

4.2 手动实现 Send 和 Sync

只有当你确定类型是安全的,才能手动实现:

rust 复制代码
struct MyType {
    data: *mut u8,   // 裸指针默认不是 Send/Sync
}

// 你承诺:MyType 可以安全地在线程间转移
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}

⚠️ 警告:手动实现 Send 和 Sync 是 unsafe 操作,必须确保类型确实安全。错误实现会导致未定义行为。

五、线程池与 Rayon:更高层的并发抽象

5.1 标准库没有线程池

Rust 标准库没有内置线程池。实践中常用 rayon 或 threadpool crate:

toml

dependencies

rayon = "1.10"

rust 复制代码
use rayon::prelude::*;

fn main() {
    let numbers: Vec<i64> = (1..=1_000_000).collect();

    // 并行求和
    let sum: i64 = numbers.par_iter().sum();
    println!("并行求和: {}", sum);

    // 并行过滤 + 映射
    let evens: Vec<i64> = numbers.par_iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 2)
        .collect();
    println!("偶数个数: {}", evens.len());
}

Rayon 的价值:只需将 iter() 改为 par_iter(),就能把迭代器链并行化。Rayon 自动管理工作线程池和任务调度。

5.2 线程池实现原理

rust 复制代码
use std::sync::{Arc, Mutex, mpsc};
use std::thread;

type Job = Box<dyn FnOnce() + Send + 'static>;

struct ThreadPool {
    workers: Vec<thread::JoinHandle<()>>,
    sender: mpsc::Sender<Job>,
}

impl ThreadPool {
    fn new(size: usize) -> Self {
        let (sender, receiver) = mpsc::channel::<Job>();
        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);
        for _ in 0..size {
            let receiver = Arc::clone(&receiver);
            workers.push(thread::spawn(move || loop {
                let job = receiver.lock().unwrap().recv();
                match job {
                    Ok(job) => job(),
                    Err(_) => break,   // Channel 关闭,退出
                }
            }));
        }

        ThreadPool { workers, sender }
    }

    fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        self.sender.send(Box::new(f)).unwrap();
    }
}

六、实战:并发 Web 爬虫

综合线程、Channel 和共享状态,实现一个并发 Web 爬虫:

rust 复制代码
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

#[derive(Debug, Clone)]
struct Page {
    url: String,
    title: String,
    links: Vec<String>,
}

/// 模拟抓取页面
fn fetch_page(url: &str) -> Page {
    thread::sleep(Duration::from_millis(100));   // 模拟网络延迟
    Page {
        url: url.to_string(),
        title: format!("页面 {}", url),
        links: vec![
            format!("{}/link1", url),
            format!("{}/link2", url),
        ],
    }
}

fn main() {
    let seed_urls = vec![
        "https://example.com".to_string(),
        "https://rust-lang.org".to_string(),
        "https://crates.io".to_string(),
    ];

    // 已访问 URL 集合(共享状态)
    let visited = Arc::new(Mutex::new(HashSet::new()));
    // 任务队列(Channel)
    let (tx, rx) = mpsc::channel::<String>();
    // 结果收集
    let (result_tx, result_rx) = mpsc::channel::<Page>();

    let rx = Arc::new(Mutex::new(rx));

    // 初始任务入队
    for url in seed_urls {
        tx.send(url).unwrap();
    }

    // 启动 4 个工作线程
    let mut workers = vec![];
    for id in 0..4 {
        let rx = Arc::clone(&rx);
        let tx = tx.clone();
        let visited = Arc::clone(&visited);
        let result_tx = result_tx.clone();

        workers.push(thread::spawn(move || {
            loop {
                // 获取任务
                let url = {
                    let rx = rx.lock().unwrap();
                    match rx.try_recv() {
                        Ok(url) => url,
                        Err(_) => break,   // 没有更多任务
                    }
                };

                // 检查是否已访问
                {
                    let mut visited = visited.lock().unwrap();
                    if visited.contains(&url) {
                        continue;
                    }
                    visited.insert(url.clone());
                }

                // 抓取页面
                println!("[工作线程 {}] 抓取: {}", id, url);
                let page = fetch_page(&url);

                // 发送结果
                result_tx.send(page.clone()).unwrap();

                // 新链接入队
                for link in &page.links {
                    let _ = tx.send(link.clone());
                }
            }
        }));
    }

    // 关闭主发送者
    drop(tx);
    drop(result_tx);

    // 等待所有工作线程结束
    for worker in workers {
        worker.join().unwrap();
    }

    // 统计结果
    let mut total_pages = 0;
    let mut total_links = 0;
    for page in result_rx {
        total_pages += 1;
        total_links += page.links.len();
    }

    println!("\n=== 爬取统计 ===");
    println!("总页面数: {}", total_pages);
    println!("总链接数: {}", total_links);
    println!("已访问 URL 数: {}", visited.lock().unwrap().len());
}

核心设计:

Arc<Mutex>:共享已访问 URL 集合,避免重复抓取。

Arc<Mutex>:多个工作线程共享任务队列。

mpsc::channel:任务分发和结果收集。

drop(tx):关闭 Channel,通知所有工作线程退出。

七、小结

线程创建:thread::spawn + move 闭包,join 等待结束。

消息传递:mpsc::channel 实现多生产者单消费者模型,sync_channel 提供背压。

共享状态:Arc<Mutex> 是跨线程共享可变数据的经典模式。

Send 和 Sync :编译期并发安全的基石,Rc 和 RefCell 不满足。

死锁避免:固定加锁顺序、try_lock、缩短锁持有时间。

Rayon:par_iter() 一行代码实现数据并行。

实战模式:工作线程 + 任务队列 + 共享状态,是并发任务的通用架构。

相关推荐
Source.Liu2 小时前
【A11】 labelprinter(Tauri 版)新建步骤
windows·rust
孙启超3 小时前
【AI开发之Rust】第 8 课:泛型、trait 与 trait 对象
开发语言·后端·rust
蓝宝石的傻话3 小时前
MiBee Eye Notebook:把闲置笔记本变成一台带麦克风的网络摄像头
数码相机·rust
小灰灰搞电子4 小时前
Rust Once 、OnceLock、LazyLock 一次性初始化详解
开发语言·后端·rust
qq_452396235 小时前
第八篇:《智能指针与内部可变性:Rust 的进阶内存管理》
rust
Amos_Web6 小时前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读
柯南46681 天前
【AI开发之Rust】第 8 课:泛型、trait 与 trait 对象
rust·编程语言
yume_sibai1 天前
12-Rust 性能优化指南(性能分析 + 内存优化 + SIMD + 并发优化 + 编译器优化 + 基准测试)
开发语言·性能优化·rust