MST

星途 面试题库

面试题:Rust线程取消机制中的内存安全考量

在设计Rust线程取消机制时,会涉及到内存安全问题。请阐述当子线程被取消时,如何确保其使用的资源(如堆内存分配)能够被正确释放,且不会引发悬空指针或内存泄漏等问题。举例说明可能出现的内存安全风险场景及对应的解决策略。
29.9万 热度难度
编程语言Rust

知识考点

AI 面试

面试题答案

一键面试

资源释放及避免风险策略

  1. 自动内存管理:Rust的所有权和借用规则,以及RAII(Resource Acquisition Is Initialization)机制在资源释放上起着关键作用。当子线程结束(包括被取消)时,栈上变量的析构函数会自动被调用,从而释放它们所管理的资源。例如,String类型在析构时会释放其内部的堆内存。
fn main() {
    let handle = std::thread::spawn(|| {
        let s = String::from("hello");
        // 线程结束时,`s` 的析构函数自动调用,释放堆内存
    });
    // 等待子线程结束
    handle.join().unwrap();
}
  1. MutexArc:如果子线程需要共享资源,Mutex(互斥锁)和 Arc(原子引用计数)是常用的工具。Arc 用于共享所有权,Mutex 用于控制对共享资源的访问。当子线程被取消时,Mutex 会确保在析构时资源的一致性。
use std::sync::{Arc, Mutex};

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let handle = std::thread::spawn({
        let data = Arc::clone(&data);
        move || {
            let mut data = data.lock().unwrap();
            data.push(4);
            // 线程结束时,`data` 析构,正确释放内存
        }
    });
    handle.join().unwrap();
}

可能出现的内存安全风险场景及解决策略

  1. 悬空指针风险场景:假设子线程创建了一个堆上对象,并将指向该对象的指针传递给主线程。如果子线程在主线程使用该指针前被取消,主线程中的指针就会悬空。
// 错误示例
use std::thread;

fn main() {
    let mut ptr: *mut i32 = std::ptr::null_mut();
    let handle = thread::spawn(|| {
        let data = Box::new(42);
        ptr = Box::into_raw(data);
    });
    handle.join().unwrap();
    // 此时 `ptr` 可能悬空,因为 `Box` 在子线程结束时已析构
    if!ptr.is_null() {
        unsafe {
            println!("{}", *ptr);
        }
    }
}

解决策略:使用 ArcWeak 指针。Arc 用于强引用,保持对象存活,Weak 指针用于弱引用,不影响对象的生命周期。当强引用计数为0(如子线程结束)时,Weak 指针可以检测到对象是否已被释放,避免悬空指针。

use std::sync::{Arc, Weak};

fn main() {
    let data_arc: Arc<i32> = Arc::new(42);
    let data_weak: Weak<i32> = Arc::downgrade(&data_arc);
    let handle = std::thread::spawn({
        let local_arc = Arc::clone(&data_arc);
        move || {
            // 子线程处理逻辑
        }
    });
    handle.join().unwrap();
    if let Some(data) = data_weak.upgrade() {
        println!("{}", data);
    } else {
        println!("对象已被释放");
    }
}
  1. 内存泄漏风险场景:如果子线程中存在手动内存分配(使用 unsafe 代码)且在取消时没有正确释放,就会导致内存泄漏。
// 错误示例
use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        let ptr = unsafe { std::alloc::alloc(std::alloc::Layout::new::<i32>()) };
        // 子线程结束时未释放 `ptr`,导致内存泄漏
    });
    handle.join().unwrap();
}

解决策略:将手动分配的内存封装在实现了 Drop 特征的结构体中,利用RAII机制确保内存正确释放。

use std::alloc::{alloc, dealloc, Layout};
use std::mem;

struct ManualAlloc {
    ptr: *mut i32,
}

impl Drop for ManualAlloc {
    fn drop(&mut self) {
        if!self.ptr.is_null() {
            unsafe {
                dealloc(self.ptr as *mut u8, Layout::new::<i32>());
            }
        }
    }
}

fn main() {
    let handle = std::thread::spawn(|| {
        let layout = Layout::new::<i32>();
        let ptr = unsafe { alloc(layout) };
        if ptr.is_null() {
            panic!("内存分配失败");
        }
        let manual_alloc = ManualAlloc { ptr: ptr as *mut i32 };
        // 线程结束时,`manual_alloc` 析构,释放内存
    });
    handle.join().unwrap();
}