Rust 多线程从入门到实战

文章目录

Rust 多线程从入门到实战

在多核处理器普及的今天,多线程编程早已成为提升程序性能的核心手段。但在多线程开发中,数据竞争、死锁、野指针等问题常常让人头疼,调试难度极大。而 Rust 凭借其独特的所有权系统和类型安全设计,将这些并发隐患扼杀在编译期,实现了无畏并发(Fearless Concurrency),让开发者既能享受多线程的性能优势,又无需担心内存安全问题。

并发与并行的区别

在开始编写多线程代码前,我们先厘清两个容易混淆的概念,这对理解Rust多线程的设计理念至关重要:

  • 并发(Concurrency):多个任务在同一时间段内交替执行,即使是单核 CPU,通过任务调度也能实现,比如交替处理多个网络请求,核心是任务切换。
  • 并行(Parallelism):多个任务在同一时刻真正同时执行,需要多核 CPU 的支持,比如多个核心同时处理不同的计算任务,核心是同时执行,是真正的多任务并行。

Rust 对两者都提供了完善的支持,而我们常说的多线程编程,既可以实现并发,也可以实现并行,具体取决于 CPU 核心数和程序调度。

创建与管理线程

最简单的多线程示例

使用 thread::spawn 可以快速创建一个新线程,该函数接收一个闭包作为参数,闭包中是新线程要执行的任务:

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

fn main() {
    // 创建新线程,执行闭包中的任务
    thread::spawn(|| {
        for i in 0..=5 {
            println!("子线程:执行第{}次", i);
            // 让线程休眠100毫秒,模拟任务耗时
            thread::sleep(Duration::from_millis(100));
        }
    });

    // 主线程执行自己的任务
    for i in 0..=2 {
        println!("主线程:执行第{}次", i);
        thread::sleep(Duration::from_millis(100));
    }

    thread::sleep(Duration::from_millis(1000));
}

运行上述代码,你会发现输出结果是乱序的,主线程和子线程在交替执行,这就是并发的体现。

等待线程完成:JoinHandle

thread::spawn 函数的返回值是一个 JoinHandle<T> 类型,其中 T 是子线程闭包的返回值类型。调用 join() 方法可以让主线程阻塞,等待子线程执行完毕,并获取子线程的返回值。

rust 复制代码
use std::thread;

fn main() {
    // 创建子线程,获取 JoinHandle
    let handle = thread::spawn(|| {
        let sum: i32 = (1..=100).sum();
        sum // 子线程返回求和结果
    });

    // 主线程等待子线程完成,并获取返回值
    // join() 返回 Result 类型,unwrap() 用于提取成功结果
    // 实际开发中需处理错误情况,避免 panic
    let result = handle.join().unwrap();

    println!("子线程计算结果:1+2+...+100 = {}", result);
}

这里有一个关键细节:Rust 的所有权系统会严格检查线程间的数据访问。如果闭包中使用了主线程的变量,必须通过 move 关键字将变量的所有权转移到子线程中,否则会编译失败。

线程与所有权:move 关键字的作用

Rust 不允许线程间共享所有权,这是为了避免数据竞争,因此当子线程需要使用主线程的变量时,必须通过 move 将变量所有权转移。如下所示:

rust 复制代码
use std::thread;

fn main() {
    let msg = String::from("Hello, Rust!");
    // 使用 move 将 msg 的所有权转移到子线程
    let handle = thread::spawn(move || {
        println!("子线程收到消息:{}", msg);
    });
    handle.join().unwrap();
    // 注意:此时 msg 的所有权已转移,主线程无法再使用 msg
}

线程间通信

多线程协作的核心是通信,线程之间需要交换数据才能完成复杂任务。Rust 提供了两种主流的线程间通信方式:消息传递和共享状态,其中消息传递是 Rust 推荐的方式,不要通过共享内存来通信,而要通过通信来共享内存。

消息传递:mpsc 通道

Rust标准库中的 std::sync::mpsc 模块(multi-producer, single-consumer)提供了"多生产者、单消费者"的通道,允许多个线程向一个线程发送消息,实现安全的线程间通信。

通道的核心是两个角色:发送者(Sender)和接收者(Receiver),通过 mpsc::channel() 函数创建,返回一个元组(Sender, Receiver)。

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

fn main() {
    // 创建通道:发送者(tx)和接收者(rx)
    let (tx, rx) = mpsc::channel();

    // 创建第一个子线程(生产者1)
    let tx1 = tx.clone(); // 克隆发送者,实现多生产者
    thread::spawn(move || {
        let messages = vec!["消息1", "消息2", "消息3"];
        for msg in messages {
            tx1.send(msg).unwrap(); // 发送消息
            thread::sleep(Duration::from_millis(500));
        }
    });

    // 创建第二个子线程(生产者2)
    let tx2 = tx.clone();
    thread::spawn(move || {
        let messages = vec!["消息A", "消息B", "消息C"];
        for msg in messages {
            tx2.send(msg).unwrap();
            thread::sleep(Duration::from_millis(500));
        }
    });

    // 主线程作为消费者,接收所有消息
    for received in rx {
        println!("主线程收到:{}", received);
    }
}

共享状态:Arc 与 Mutex 的组合

虽然消息传递是推荐方式,但在某些场景下,比如多个线程需要读写同一个变量,我们需要使用共享状态。Rust 通过原子引用计数(Arc)和互斥锁(Mutex)的组合,实现安全的共享状态访问。

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

fn main() {
    // 创建共享计数器:Arc 包裹 Mutex,确保线程安全
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    // 创建10个线程,每个线程对计数器加1
    for _ in 0..10 {
        // 克隆 Arc,仅克隆引用,不复制数据
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 获取锁,返回 MutexGuard,实际开发中需处理可能的锁定失败,这里简化使用 unwrap()
            let mut num = counter.lock().unwrap();
            *num += 1;
            // MutexGuard 离开作用域时,自动释放锁
        });
        handles.push(handle);
    }

    // 等待所有线程完成
    for handle in handles {
        handle.join().unwrap();
    }

    // 最终结果:10个线程各加1,结果为10
    println!("最终计数器值:{}", *counter.lock().unwrap());
}

除了 Mutex,Rust 还提供了 RwLock(读写锁),适用于读多写少的场景,即多个线程可同时读取数据,只有一个线程能写入数据,能提升并发性能。

进阶:原子类型与 Send/Sync 特征

原子类型:无锁并发

对于简单的共享变量(如计数器),使用 Mutex 会有一定的性能开销(锁的获取和释放)。Rust 标准库中的 std::sync::atomic 模块提供了原子类型(如 AtomicUsize、AtomicBool),无需加锁即可实现线程安全的读写操作,性能更优。

rust 复制代码
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

fn main() {
    // 创建原子计数器,初始值为0
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 原子递增,使用 Relaxed 表示不保证内存顺序
            counter.fetch_add(1, Ordering::Relaxed);
        });
        handles.push(handle);
    }

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

    // 读取原子类型的值
    println!("原子计数器值:{}", counter.load(Ordering::Relaxed));
}

Send 与 Sync:Rust 的线程安全标记

Rust 通过两个特殊的特征(trait)来标记类型的线程安全性,编译器会自动检查,确保线程间的数据访问安全:

  • Send:表示类型的所有权可以安全地在线程间转移(如 StringArc)。
  • Sync:表示类型的引用可以安全地在多个线程间共享(如 Mutex<T>、原子类型)。

大多数 Rust 标准类型(如 i32Vec<T>)都自动实现了 SendSync,但也有例外:

  • Rc<T>:未实现 SendSync,只能用于单线程。
  • 裸指针(*const T*mut T):未实现 SendSync,需手动保证安全。

无需手动为类型实现 SendSync,编译器会自动推导,只有当你实现自定义同步原语时,才需要手动实现,并确保其线程安全。

总结

Rust 的多线程设计看似复杂,实则是为了安全与性能的平衡。只要掌握了所有权和同步原语的核心逻辑,就能轻松编写高效、安全的多线程程序。后续可以深入学习异步编程,进一步提升并发性能。

相关推荐
卷毛的技术笔记2 小时前
从“拆东墙补西墙”到“最终一致”:分布式事务在Spring Boot/Cloud中的破局之道
java·spring boot·分布式·后端·spring cloud·面试·rocketmq
Ulyanov2 小时前
《玩转QT Designer Studio:从设计到实战》 QT Designer Studio数据绑定与表达式系统深度解析
开发语言·python·qt
袋鱼不重2 小时前
Hermes Agent 直连飞书机器人
前端·后端·ai编程
Pkmer2 小时前
古法编程: 深度解析Java调度器Timer
java·后端
小强19882 小时前
C++23/26新特性解析:那些让你放弃Boost库的杀手锏
后端
Aolith2 小时前
学 Express 被 app.use 绕晕了?用流水线思维一次性搞懂 5 种中间件
后端·express
BduL OWED2 小时前
将 vue3 项目打包后部署在 springboot 项目运行
java·spring boot·后端
二月龙2 小时前
从C++到WebAssembly:让高并发计算跑在浏览器里
后端
ZJY1322 小时前
3-12:路由和重构
后端·node.js