Rust 中 Mutex、RwLock 的使用实例

在多线程编程中,同步访问共享数据是一个常见的需求。Rust 标准库的 std::sync::Mutex 提供互斥锁。

导入依赖

首先,我们需要导入标准库中的 std::sync::Mutex

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

Arc 也是必要的,因为在多线程环境下,需要使用 Arc 来安全地共享 Mutex

创建 Mutex

1let mutex = Mutex::new(0);

这里创建了一个 Mutex 包裹的可变整数,并初始化为 0。

使用 Mutex

获取锁并修改数据

要访问被 Mutex 保护的数据,首先需要获取锁。可以使用 lock 方法来获取锁,并在作用域内处理数据:

1{
2    let mut data = mutex.lock().unwrap();
3    *data += 1;
4} // 在这里锁会被自动释放

在这个例子中,lock 方法返回一个 MutexGuard,它是一个 RAII 结构,在作用域结束时会自动释放锁。通过解引用操作符 * 可以获取被保护数据的可变引用,然后进行操作。

使用 Arc 包装 Mutex

如果要在多个线程中共享 Mutex,则需要使用 Arc 来保证安全性。Arc 是原子引用计数的智能指针,可以安全地跨线程共享所有权。

 1let mutex = Arc::new(Mutex::new(0));
 2let mut handles = vec![];
 3
 4for _ in 0..10 {
 5    let mutex = Arc::clone(&mutex);
 6    let handle = std::thread::spawn(move || {
 7        let mut data = mutex.lock().unwrap();
 8        *data += 1;
 9    });
10    handles.push(handle);
11}
12
13for handle in handles {
14    handle.join().unwrap();
15}

在这个例子中,Arc::clone 会增加引用计数,并在每个线程中传递 Arc 的所有权,确保 Mutex 在不同线程中的共享。

使用 read 方法

如果只需要读取 Mutex 中的数据,可以使用 read 方法获取读锁,这样多个线程可以同时读取数据,而不会阻塞。

1let data = mutex.read().unwrap();
2println!("Data: {}", *data);

错误处理

在获取锁时,需要处理可能的错误。lock 方法返回一个 Result 类型,如果获取锁失败,会返回 Err

1let data = mutex.lock();
2if let Ok(mut data) = data {
3    *data += 1;
4} else {
5    println!("Failed to acquire lock!");
6}

综合示例

 1use std::collections::HashMap;
 2use std::fs;
 3use std::fs::File;
 4use std::io::{self, Read, Write};
 5use std::sync::{Arc, Mutex, RwLock};
 6
 7struct FileManager {
 8    locks: Mutex<HashMap<String, Arc<RwLock<()>>>>,
 9}
10
11impl FileManager {
12    fn new() -> Self {
13        FileManager {
14            locks: Mutex::new(HashMap::new()),
15        }
16    }
17
18    fn read_file(&self, file_path: &str) -> io::Result<Vec<u8>> {
19        let mut buf = Vec::new();
20        {
21            let lock_ref: Arc<RwLock<()>>= {
22                let mut locks = self.locks.lock().unwrap();
23				Arc::clone(&locks.entry(file_path.to_string()).or_default())
24            };
25            let _read_guard = lock_ref.read().unwrap();
26
27            let mut file = File::open(file_path)?;
28            file.read_to_end(&mut buf)?;
29        }
30        Ok(buf)
31    }
32
33    fn write_file(&self, file_path: &str, data: &[u8]) -> io::Result<()> {
34        let lock_ref = {
35            let mut locks = self.locks.lock().unwrap();
36            Arc::clone(locks.entry(file_path.to_string()).or_default())
37        };
38        let _write_guard = lock_ref.write().unwrap();
39
40        let mut file = File::create(file_path)?;
41        file.write_all(data)?;
42        Ok(())
43    }
44}
45
46use std::thread;
47fn main() {
48    let manager = Arc::new(FileManager::new());
49    let file_path = "example.txt";
50
51    let mut handles = vec![];
52
53    for i in 0..10 {
54        let manager_clone = Arc::clone(&manager);
55        let path = file_path.to_string();
56        handles.push(thread::spawn(move || {
57            manager_clone.write_file(&path, format!("Hello from thread {}!\n", i).as_bytes()).expect("Write failed");
58        }));
59    }
60
61    for _ in 0..10 {
62        let manager_clone = Arc::clone(&manager);
63        let path = file_path.to_string();
64        handles.push(thread::spawn(move || {
65            let data = manager_clone.read_file(&path).expect("Read failed");
66            println!("Read from thread: {}", String::from_utf8_lossy(&data));
67        }));
68    }
69
70    for handle in handles {
71        handle.join().unwrap();
72    }
73}

这个例子中,FileManager 类使用了 Rust 的标准库中的 MutexRwLock 来实现对文件的并发读写控制。

使用的方法

  1. Mutex:

    • Mutex::new(): 初始化一个新的互斥锁。

    • lock(): 对互斥锁进行加锁,返回一个包含锁定资源的 MutexGuard

    • unwrap(): 在 MutexGuard 上调用,用于处理可能的 PoisonError(当持有锁的线程崩溃时发生)。

  2. RwLock

    • read(): 获取读锁,返回一个 RwLockReadGuard,允许多个线程同时读取数据但不允许写入。

    • write(): 获取写锁,返回一个 RwLockWriteGuard,只允许一个线程写入数据并阻止其他读写操作。

使用技巧

  1. 锁的层次化管理

    • FileManager 使用一个 Mutex<HashMap<String, Arc<RwLock<()>>>> 来管理对各个文件的访问。这里的 HashMap 映射了文件路径到一个 RwLockArc(原子引用计数指针)。这种设计允许对每个文件独立地进行读写锁控制。
  2. 避免死锁

    • read_filewrite_file 方法中,首先锁定整个 HashMap,然后只在需要时获取对应文件的 RwLock。这样的设计减少了持锁时间,尽快释放 Mutex 锁,从而减少死锁的可能性。
  3. 资源共享与优化

    • 使用 Arc 允许多个线程共享对 RwLock 的访问权,而 RwLock 又允许多个线程同时读取同一个资源,只在写入时限制为单线程操作。这种设计有效地提高了并发性能。

另外,注意看 let _read_guard = lock_ref.read().unwrap(); 这行代码能够获得一个读锁,后面的语句可以在锁的保护下运行。正常情况我们是基于 lock_ref.read() 所包含的数据来操作,因此不用声明一个”未使用“变量,但这行代码,我们知识利用了读锁,并不直接用读锁内的数据。