MST

星途 面试题库

面试题:Rust闭包并发安全设计中的死锁与资源管理

在复杂的Rust并发程序中,使用闭包时可能会出现死锁情况。请描述一种可能导致死锁的场景,该场景涉及闭包、多线程和资源管理。并阐述如何通过合理的设计(如使用 `Mutex`、`RwLock` 等)来避免这种死锁,同时保证闭包的并发安全。请结合具体的代码实现和理论分析进行说明。
25.8万 热度难度
编程语言Rust

知识考点

AI 面试

面试题答案

一键面试

可能导致死锁的场景

假设有两个线程 thread1thread2,每个线程都尝试获取两个不同的资源 resource1resource2,并且获取资源的操作封装在闭包中。如果 thread1 先获取 resource1,然后尝试获取 resource2,而 thread2 先获取 resource2,然后尝试获取 resource1,就可能会发生死锁。

以下是示例代码:

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

fn main() {
    let resource1 = Arc::new(Mutex::new(0));
    let resource2 = Arc::new(Mutex::new(0));

    let resource1_clone = Arc::clone(&resource1);
    let resource2_clone = Arc::clone(&resource2);

    let handle1 = thread::spawn(move || {
        let _lock1 = resource1_clone.lock().unwrap();
        // 模拟一些工作
        thread::sleep(std::time::Duration::from_millis(100));
        let _lock2 = resource2_clone.lock().unwrap();
    });

    let handle2 = thread::spawn(move || {
        let _lock2 = resource2.lock().unwrap();
        // 模拟一些工作
        thread::sleep(std::time::Duration::from_millis(100));
        let _lock1 = resource1.lock().unwrap();
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

在这个例子中,thread1 先锁定 resource1,再锁定 resource2,而 thread2 先锁定 resource2,再锁定 resource1。如果 thread1 在锁定 resource1 后,thread2 在锁定 resource2 后,双方都尝试获取对方已锁定的资源,就会导致死锁。

避免死锁的设计及理论分析

为了避免死锁,可以采用资源获取顺序一致的原则。即所有线程都按照相同的顺序获取资源。

以下是修改后的代码,使用 Mutex 来保证资源安全,并遵循一致的资源获取顺序:

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

fn main() {
    let resource1 = Arc::new(Mutex::new(0));
    let resource2 = Arc::new(Mutex::new(0));

    let resource1_clone = Arc::clone(&resource1);
    let resource2_clone = Arc::clone(&resource2);

    let handle1 = thread::spawn(move || {
        let _lock1 = resource1_clone.lock().unwrap();
        // 模拟一些工作
        thread::sleep(std::time::Duration::from_millis(100));
        let _lock2 = resource2_clone.lock().unwrap();
    });

    let handle2 = thread::spawn(move || {
        let _lock1 = resource1.lock().unwrap();
        // 模拟一些工作
        thread::sleep(std::time::Duration::from_millis(100));
        let _lock2 = resource2.lock().unwrap();
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

理论分析:通过让所有线程都先获取 resource1,再获取 resource2,就不会出现相互等待对方释放资源的情况,从而避免了死锁。

如果资源读写操作较多,可以考虑使用 RwLockRwLock 允许多个线程同时读,但只允许一个线程写。在使用 RwLock 时,同样要遵循一致的资源获取顺序。

示例代码如下:

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

fn main() {
    let resource1 = Arc::new(RwLock::new(0));
    let resource2 = Arc::new(RwLock::new(0));

    let resource1_clone = Arc::clone(&resource1);
    let resource2_clone = Arc::clone(&resource2);

    let handle1 = thread::spawn(move || {
        let _read_lock1 = resource1_clone.read().unwrap();
        // 模拟读操作
        thread::sleep(std::time::Duration::from_millis(100));
        let _read_lock2 = resource2_clone.read().unwrap();
    });

    let handle2 = thread::spawn(move || {
        let _read_lock1 = resource1.lock().unwrap();
        // 模拟写操作
        thread::sleep(std::time::Duration::from_millis(100));
        let _read_lock2 = resource2.lock().unwrap();
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

在这个示例中,无论是读操作还是写操作,线程都按照相同的顺序获取资源,避免了死锁,同时 RwLock 也保证了并发读写的安全。