Skip to content

多线程

官方文档“无畏并发”的标题翻译听起来像“无谓”...所以莫名感觉有点狂🤨

生(spawn)一个线程

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    // 无论线程是否执行完毕,一旦主线程结束,多线程任务也会中断
    // 此时可以使用join阻塞主线程,等待 handle 线程结束
    handle.join().unwrap();
}

消息传递

“不要通过共享内存来通信;相反,通过通信来共享内存。”

通道是一个通用的编程概念,有发送端和接收端两部分,通过它数据可以从一个线程发送到另一个线程。

创建通道

rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();	// 创建一个通道,并将两个端点分别赋值给 tx(发送)和 rx(接收)

    thread::spawn(move || {	// move将tx移动到闭包中
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

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

克隆并使用发送端

多生产者,单消费者:Rust 标准库实现通道的方式意味着一个通道可以有多个发送端产生值,但只有一个接收端消费这些值

rust
// --snip--

// 创建消息通道:返回一个发送端(tx)和一个接收端(rx)
// mpsc = Multiple Producer, Single Consumer (多生产者,单消费者)
let (tx, rx) = mpsc::channel();

// 克隆发送端(tx),创建第二个发送端(tx1)
// 现在有两个发送端:tx 和 tx1(多个生产者)
let tx1 = tx.clone();

// 启动第一个生产者线程
thread::spawn(move || {
    // 该线程拥有 tx1 的所有权(move 转移所有权)
    let vals = vec![
        String::from("hi"),
        String::from("from"),
        String::from("the"),
        String::from("thread"),
    ];

    // 循环发送消息
    for val in vals {
        // 通过 tx1 发送字符串消息
        tx1.send(val).unwrap();
        // 每次发送后暂停1秒
        thread::sleep(Duration::from_secs(1));
    }
}); // 线程结束,tx1 自动释放

// 启动第二个生产者线程
thread::spawn(move || {
    // 该线程拥有原始 tx 的所有权(move 转移所有权)
    let vals = vec![
        String::from("more"),
        String::from("messages"),
        String::from("for"),
        String::from("you"),
    ];

    for val in vals {
        // 通过 tx 发送消息
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
}); // 线程结束,tx 自动释放

// 在主线程中接收消息(单消费者)
// rx 是通道的接收端,只能有一个接收端
for received in rx {
    // 从通道接收消息(自动阻塞等待)
    println!("Got: {received}");
} // 当所有发送端关闭后,循环自动结束

// --snip--

共享状态(共享环境)

这里终于轮到 Mutex<T> 和 Arc<T> 登场了!

  • Mutex<T>:一个提供了内部可变性的互斥锁(容器)。

    • 像银行金库的门:一次只允许一人进入
    • 通过 .lock() 获取访问权(钥匙)
    • 离开作用域时自动释放锁(还钥匙)
  • Arc<T>:原子引用计数(使用了标准库atomic实现的引用计数指针),一个线程安全的智能指针。

    • 让多个线程安全共享数据所有权
    • 跟踪有多少线程在使用数据
    • 当最后一个使用者退出时自动清理

这俩个东东的使用方法和 Rc<T> 、RefCell<T> 一样。

补充:

  • mutex离开作用域后会自动释放锁
  • 锁在被释放之后才可以被申请到
  • mutex申请锁时是阻塞运行的,直到拿到锁为止

使用方式

rust
// 1. 创建受保护数据
let data = Arc::new(Mutex::new(共享值));

// 2. 克隆指针供线程使用
let thread_data = Arc::clone(&data);

// 3. 在线程中获取锁
let mut guard = thread_data.lock().unwrap();

// 4. 操作数据
*guard = 新值;

// 5. 自动释放锁(离开作用域时)

例程

多个线程同时更新同一个银行账户余额的场景:

rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 创建共享银行账户,初始余额100
    // Arc = Atomic Reference Count(原子引用计数),允许跨线程共享所有权
    // Mutex = Mutual Exclusion(互斥锁),保证同时只有一个线程访问数据
    let account = Arc::new(Mutex::new(100)); // 账户余额: 100元
    
    println!("初始余额: {}", account.lock().unwrap());

    // 创建线程句柄存储
    let mut handles = vec![];

    // 模拟5个ATM同时操作账户
    for i in 0..5 {
        // 克隆账户的智能指针(增加引用计数)
        let acc_clone = Arc::clone(&account);
        
        // 每个ATM线程存入不同金额
        let handle = thread::spawn(move || {
            // 存款金额:第1个ATM存10元,第2个存20元...
            let deposit_amount = (i + 1) * 10;
            
            // 获取锁(阻塞直到获得访问权)
            let mut balance = acc_clone.lock().unwrap();
            
            // 安全更新余额
            *balance += deposit_amount;
            println!("ATM{}存入: {}元", i, deposit_amount);
            
            // 锁在作用域结束时自动释放
        });
        
        handles.push(handle);
    }

    // 等待所有ATM操作完成
    for handle in handles {
        handle.join().unwrap();
    }

    // 获取最终余额
    let final_balance = account.lock().unwrap();
    println!("最终余额: {}元", *final_balance);
    // 预期结果: 100 + (10+20+30+40+50) = 250元
}

Send、Sync trait

"线程安全"指特定数据可以被多个并发线程安全使用。实现了Send、Sync的类型自带线程安全。

更确切地说,这俩是标记类 trait,位于std::marker,类型不用实现具体代码,这更像是语言特性,不像标准库提供的其他东西。

  • send 可以将所有权在线程间传送
  • sync 可以直接在多个线程中直接使用引用访问

分开 Send 和 Sync 特性的原因:一个类型可能是其中之一,两者都是,或两者都不是

  • Rc<T>:既不是 Send 也不是 Sync
  • RefCell<T>:是Send(如果T是Send),但不是 Sync·Mutex<T>:是Send也是 Sync,可用于多线程共享访问
  • MutexGuard<'a,T>:是Sync(如果T是Sync),但不是 Send