Rust - 多线程共享 Mutex<T>

126 阅读4分钟

1. 为什么要共享 Mutex<T>

想象你和几个朋友一起用一台公共电脑:

  • 问题:如果大家同时用键盘鼠标,电脑会乱套
  • 解决方案:给电脑配个"使用牌",谁拿到牌子谁用

在 Rust 中:

  • 公共电脑 = 需要共享的数据
  • 使用牌 = Mutex<T>
  • 朋友们 = 多个线程

2. 如何共享 Mutex<T>

2.1 基本步骤

  1. 创建需要保护的数据
  2. 用 Mutex 包装它
  3. 用 Arc(原子引用计数)包装 Mutex
  4. 克隆 Arc 给每个线程

2.2 具体代码

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

fn main() {
    // 1. 创建需要共享的数据(这里是个计数器)
    let counter = Arc::new(Mutex::new(0)); // 包装两层
    
    let mut handles = vec![]; // 保存线程句柄

    for _ in 0..10 {
        // 2. 克隆Arc(增加引用计数)
        let counter = Arc::clone(&counter);
        
        // 3. 创建线程
        let handle = thread::spawn(move || {
            // 4. 获取锁
            let mut num = counter.lock().unwrap();
            
            // 5. 修改数据
            *num += 1;
            
            // 6. 锁会自动释放(当num离开作用域)
        });
        
        handles.push(handle);
    }

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

    // 查看最终结果
    println!("最终计数: {}", *counter.lock().unwrap());
    // 正确输出: 10(因为有10个线程各+1)
}

3. 为什么需要这样设计?

3.1 为什么用 Arc

  • Arc = Atomic Reference Count(原子引用计数)
  • 就像"使用牌"的登记本,记录有多少人想用电脑
  • 每个线程拿到自己的"登记副本"(克隆Arc)
  • 当所有线程用完,登记本归零,数据自动清理

3.2 为什么用 Mutex?

  • 确保同一时间只有一个线程能修改数据
  • 防止"数据竞争"(多个线程同时写导致数据错乱)

4. 实际应用场景

场景1:银行账户系统

struct BankAccount {
    balance: f64,
    owner: String,
}

let account = Arc::new(Mutex::new(BankAccount {
    balance: 1000.0,
    owner: "张三".to_string(),
}));

// 多个ATM线程同时操作账户
for _ in 0..5 {
    let acc = Arc::clone(&account);
    thread::spawn(move || {
        let mut acc = acc.lock().unwrap();
        acc.balance -= 100.0; // 取款100元
    });
}

场景2:游戏服务器

let player_positions = Arc::new(Mutex::new(HashMap::new()));

// 每个玩家一个线程更新位置
for player_id in 0..10 {
    let positions = Arc::clone(&player_positions);
    thread::spawn(move || {
        let mut pos = positions.lock().unwrap();
        pos.insert(player_id, (rand::random(), rand::random()));
    });
}

5. 工作原理图解

主线程
│
├─ 创建 Arc<Mutex<T>> 
│  (引用计数=1)
│
├─ 克隆Arc → 线程1 (引用计数=2)
│   ├─ lock() 获取锁
│   ├─ 修改数据
│   └─ 自动释放锁
│
├─ 克隆Arc → 线程2 (引用计数=3)
│   ├─ 尝试lock() 
│   ├─ 等待线程1释放
│   ├─ 获取锁
│   └─ 修改数据
│
└─ 等待所有线程...
   最后引用计数归零,自动清理

6. 常见问题解答

Q: 为什么不直接用 Mutex 而要套 Arc? A: 因为:

  • Mutex 本身不能安全地跨线程共享所有权
  • Arc 负责线程安全的引用计数
  • Mutex 负责线程安全的数据访问

Q: 可以共享没有 Mutex 的 Arc<T> 吗? A: 可以,但只能用于只读数据。要修改数据必须加 Mutex 或 RwLock。

Q: 为什么 lock() 返回的是 LockResult? A: 因为:

  1. 可能获取锁失败(比如持有锁的线程 panic 了)
  2. 需要使用 unwrap()? 处理可能的错误

Q: 这样会影响性能吗? A: 会有一定开销:

  • Arc 的克隆/销毁有原子操作开销
  • Mutex 的 lock/unlock 有系统调用开销
  • 但在合理使用下(不频繁争抢锁)开销很小

7. 实用技巧

7.1 缩小锁范围

// 不好:锁住整个函数
let data = lock.lock().unwrap();
long_running_operation(&data);

// 好:只锁必要部分
let value = {
    let data = lock.lock().unwrap();
    data.clone() // 复制数据到锁外
};
long_running_operation(&value);

7.2 避免死锁

// 危险:嵌套获取多个锁
let lock1 = Arc::new(Mutex::new(0));
let lock2 = Arc::new(Mutex::new(0));

// 线程A
let _a = lock1.lock().unwrap();
let _b = lock2.lock().unwrap(); // 如果线程B以相反顺序获取,会死锁

// 解决方案:总是按固定顺序获取锁

7.3 使用 try_lock

match lock.try_lock() {
    Ok(guard) => {
        // 成功获取锁
    }
    Err(_) => {
        // 锁被占用,做其他事情
    }
}

8. 总结

共享 Mutex<T> 的要点:

  1. Arc 是快递员:安全地把数据"快递"给各个线程
  2. Mutex 是门卫:确保每次只有一个人能修改数据
  3. 配合使用Arc<Mutex<T>> 是最常见的线程安全共享模式

使用场景:

  • 任何需要多个线程修改同一数据的场合
  • 特别是写操作频繁的场景(相比 RwLock)
线程安全共享 = Arc + Mutex/RwLock + 合理的作用域控制