1. Rust并发基础:线程模型、Send与Sync trait、Arc与Mutex使用、原子类型入门

并发编程,说白了就是让程序同时干多件事。很多语言里这是噩梦——数据竞争、死锁、悬空指针,随便一个就能让你加班到凌晨。但Rust不一样,它用一套类型系统把这些问题扼杀在编译期。今天我们就来聊聊Rust的并发基础,这些东西我在实际项目中几乎每天都在用。

1.1 线程模型:轻量级与系统级

Rust的线程是1:1模型,也就是一个Rust线程对应一个OS线程。这和Go的M:N模型不同。我个人习惯用std::thread::spawn来创建线程,简单直接。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("子线程: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

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

    handle.join().unwrap();
}

嗯,这里要注意:join()会阻塞当前线程,直到子线程结束。如果你不调用join,主线程可能先跑完,子线程就被直接杀了。我在项目中遇到过这种问题——日志还没写完,线程就被回收了,排查了半天才发现是忘了join

小技巧:thread::sleep 控制执行顺序时,别依赖它做同步。它只是让出CPU时间片,不是锁。

1.2 Send与Sync trait:编译器的安全网

这两个trait是Rust并发安全的基石。你想想看,为什么Rust能保证线程安全?答案就在这两个标记trait里。

  • Send:类型的所有权可以在线程间转移。比如StringVec<T>都是Send的。
  • Sync:类型的引用可以在线程间共享。比如i32bool都是Sync的。

说白了,Send管的是「能不能把东西扔给另一个线程」,Sync管的是「能不能让多个线程同时看这个东西」。大部分类型默认都是Send+Sync的,但有些例外——比如Rc<T>就不是Send也不是Sync,因为引用计数不是原子操作。

避坑指南: 我曾经在项目里用 Rc<RefCell<T>> 做共享状态,结果编译报错说不是Send。后来换成 Arc<Mutex<T>> 才搞定。记住:单线程用Rc,多线程用Arc。

1.3 Arc与Mutex:共享状态的正确姿势

Arc是原子引用计数,Mutex是互斥锁。两者结合,就能安全地在多线程间共享可变数据。

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

这里有个细节:lock()返回一个MutexGuard,它实现了DerefDrop。当MutexGuard离开作用域时,锁自动释放。你想想看,这比手动unlock安全多了吧?

核心要点: Arc负责「多个线程都能持有所有权」,Mutex负责「同一时间只有一个线程能修改数据」。两者缺一不可。

1.4 原子类型入门:无锁编程的第一步

原子类型是比Mutex更轻量的同步原语。它们直接利用CPU指令保证操作的原子性,不需要操作系统介入。Rust标准库提供了std::sync::atomic模块。

原子类型 对应普通类型 常用场景
AtomicBool bool 标志位、开关
AtomicIsize isize 计数器、索引
AtomicUsize usize 计数器、长度
AtomicPtr<T> *mut T 无锁数据结构
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

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

    for _ in 0..10 {
        let counter = &counter;
        let handle = thread::spawn(move || {
            counter.fetch_add(1, Ordering::SeqCst);
        });
        handles.push(handle);
    }

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

    println!("结果: {}", counter.load(Ordering::SeqCst));
}

这里有个坑:Ordering参数控制内存顺序。我刚开始用的时候全写SeqCst,后来发现性能瓶颈就在这。实际上,大部分场景用Acquire/Release就够了,Relaxed只在计数器这种场景下安全。

避坑指南: 我曾经在项目里用 AtomicBool 做「是否初始化」的标志位,结果因为用了 Relaxed 导致另一个线程读到了未初始化的数据。后来改成 Acquire/Release 才解决。内存顺序不是玄学,是实打实的硬件行为。

知识体系图

Rust并发基础知识体系 Rust并发基础 线程模型 Send / Sync trait Arc + Mutex 原子类型 std::thread 1:1模型 所有权转移 引用共享 原子引用计数 互斥锁 AtomicBool AtomicUsize Ordering

这张图把今天的内容串起来了。从线程模型出发,到Send/Sync保证类型安全,再到Arc+Mutex实现共享可变状态,最后用原子类型做无锁优化。每一步都是Rust并发编程的基石。

我的建议: 刚开始学并发,别急着用原子类型。先用Arc+Mutex把功能跑通,性能不够再优化。我见过太多人一上来就写无锁代码,结果bug比功能还多。

好了,今天的内容就到这里。记住:Rust的并发安全不是靠运行时检查,而是靠类型系统在编译期就帮你排雷。用好Send、Sync、Arc、Mutex和原子类型,你的多线程代码就能既安全又高效。


专注资料整理