bigywater.tech
Rust · 34

Mutex<T> 与内部可变性

Mutex 是线程之间「共享且可修改」的标准方案,它把锁和数据绑在一起 —— 不拿锁就拿不到数据。这篇验证 guard 的生命周期、锁中毒的行为、两种真实的死锁写法(包括一个很隐蔽的),以及持锁时间对并发度的影响。

poisoned
持锁线程 panic 后 is_poisoned() = true,数据仍可 into_inner
3 s 超时
同一线程重复 lock 的程序在 3 秒后仍未结束(死锁)
229 vs 60 ms
4 个线程持锁睡 50 ms 与锁外睡 50 ms 的总耗时

guard 与解锁

实测 · 本机执行输出
  源码:
      1 | use std::sync::Mutex;
      2 | fn main() {
      3 |     let m = Mutex::new(vec![1, 2, 3]);                 // m 不是 mut
      4 |     {
      5 |         let mut g = m.lock().unwrap();                 // 拿到 guard
      6 |         g.push(4);
      7 |         println!("持有 guard 时 try_lock() 失败:{}", m.try_lock().is_err());
      8 |     }                                                   // guard 在这里 drop,自动解锁
      9 |     let free = m.try_lock().is_ok();                  // 单独一条语句:临时的 Result<MutexGuard> 在语句结束时释放
     10 |     println!("guard 释放之后 try_lock() 成功:{},值 = {:?}", free, m.lock().unwrap());
     11 |     *m.lock().unwrap() = vec![9];                      // 临时 guard,语句结束就释放
     12 |     println!("{:?}", m.lock().unwrap());
     13 | }
  编译:成功
  运行(退出码 0):
    持有 guard 时 try_lock() 失败:true
    guard 释放之后 try_lock() 成功:true,值 = [1, 2, 3, 4]
    [9]

中毒

实测 · 本机执行输出
  源码:
      1 | use std::sync::{Arc, Mutex};
      2 | use std::thread;
      3 | fn main() {
      4 |     let m = Arc::new(Mutex::new(5));
      5 |     let m2 = Arc::clone(&m);
      6 |     let h = thread::spawn(move || { let _g = m2.lock().unwrap(); panic!("持锁时 panic"); });
      7 |     println!("子线程的 join 结果是 Err:{}", h.join().is_err());
      8 |     println!("is_poisoned() = {}", m.is_poisoned());
      9 |     match m.lock() {
     10 |         Ok(g) => println!("拿到了 {}", *g),
     11 |         Err(e) => { println!("lock() 返回 Err(PoisonError);里面的数据仍然可以取出:{}", *e.into_inner()); }
     12 |     }
     13 | }
  编译:成功
  运行(退出码 0):
    子线程的 join 结果是 Err:true
    is_poisoned() = true
    lock() 返回 Err(PoisonError);里面的数据仍然可以取出:5
    
    thread '<unnamed>' (7002078) panicked at <实验目录>/src/u34b.rs:6:66:
    持锁时 panic
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

死锁

实测 · 本机执行输出
  程序 u34c:
      1 | use std::sync::Mutex;
      2 | fn main() {
      3 |     let m = Mutex::new(0);
      4 |     let _a = m.lock().unwrap();
      5 |     println!("第一次 lock 成功,准备第二次 lock ...");
      6 |     let _b = m.lock().unwrap();
      7 |     println!("不会打印");
      8 | }
    3 秒后仍未结束(被超时杀掉);已有的输出: '第一次 lock 成功,准备第二次 lock ...'
  程序 u34c2:
      1 | use std::sync::Mutex;
      2 | fn main() {
      3 |     let m = Mutex::new(0);
      4 |     // try_lock() 返回的 Result<MutexGuard> 是临时值,要到整条 println! 语句结束才 drop
      5 |     println!("{} {}", m.try_lock().is_ok(), m.lock().unwrap());
      6 | }
    3 秒后仍未结束(被超时杀掉);已有的输出: ''

临时值的存活范围在 edition 2024 的 if let 里有变化(见if let 与 let else)。

锁的粒度

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    4 个线程各在锁内睡 50 ms:总耗时约 229 ms(串行,≈ 4 × 50)
    4 个线程在锁外睡 50 ms、只在锁内加 1:总耗时约 60 ms(并行,≈ 50)

读多写少时可以用 RwLock,见Mutex 与 RwLock。

大小

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    u32 = 4,Mutex<u32> = 16

分步演示

Mutex<T>
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。耗时为单次运行,包含线程创建开销
    • 没有验证:parking_lot 的 Mutex(无中毒、更小)、公平性与饥饿、锁顺序死锁(两把锁互相等)、MutexGuard 不是 Send 的编译错误、Condvar 配合使用(见 Condvar 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号