MST

星途 面试题库

面试题:深入理解Rust引用可变性与并发安全的底层原理

在Rust中,编译器通过哪些机制来保证引用可变性的并发安全?例如,它是如何跟踪借用关系并防止数据竞争的?请从编译器对生命周期、所有权和借用规则的检查等方面详细阐述,并举例说明在一些边界情况下(如跨函数跨线程的复杂借用场景)编译器的处理方式。
21.7万 热度难度
编程语言Rust

知识考点

AI 面试

面试题答案

一键面试

1. 生命周期、所有权和借用规则基础

  • 所有权:Rust 中的每个值都有一个所有者,且在同一时间只有一个所有者。当所有者离开作用域时,值会被销毁。例如:
{
    let s = String::from("hello"); // s 是 "hello" 字符串的所有者
} // s 离开作用域,字符串被销毁
  • 借用:允许在不转移所有权的情况下使用值。有两种借用:不可变借用(&T)和可变借用(&mut T)。借用规则规定:
    • 同一时间,要么只能有一个可变借用,要么可以有多个不可变借用。
    • 借用的生命周期必须小于等于被借用值的生命周期。
let s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 多个不可变借用是允许的
// let r3 = &mut s; // 错误:存在不可变借用时,不能有可变借用
  • 生命周期:是指值在程序中存在的时间段。Rust 编译器使用生命周期标注来确保借用关系的有效性。例如:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这里的 'a 是生命周期参数,它表明函数 longest 的返回值的生命周期与参数 xy 中较短的那个相同。

2. 保证并发安全

  • 编译器检查:通过上述所有权、借用和生命周期规则,Rust 编译器在编译时就能检测到大多数数据竞争情况。在并发场景下,这些规则同样适用。例如,当使用 std::thread::spawn 创建新线程时:
use std::thread;

fn main() {
    let mut data = String::from("hello");
    // 错误:data 是可变的,且下面尝试将其不可变借用移动到新线程中
    // thread::spawn(move || {
    //     println!("{}", &data);
    // }).join().unwrap();
    // 正确做法:先创建不可变借用,再将其移动到新线程
    let data_ref = &data;
    thread::spawn(move || {
        println!("{}", data_ref);
    }).join().unwrap();
}
  • 跨函数借用:在函数间传递借用时,编译器会检查借用的生命周期是否正确。例如:
fn first<'a>(s: &'a str) -> &'a str {
    s
}

fn second<'a>(s: &'a str) -> &'a str {
    let sub = &s[0..3];
    sub
}

fn main() {
    let s = String::from("hello");
    let result1 = first(&s);
    let result2 = second(result1);
    println!("{}", result2);
}

这里 firstsecond 函数的生命周期参数确保了借用的有效性。

3. 跨线程复杂借用场景

  • SendSync 标记 trait
    • Send:如果一个类型实现了 Send,意味着该类型的值可以安全地转移到其他线程。大多数 Rust 类型都实现了 Send,但像 Rc<T> 这种引用计数类型没有实现,因为多个线程可能同时修改引用计数导致数据竞争。
    • Sync:如果一个类型实现了 Sync,意味着该类型的值可以在多个线程间安全共享。例如 &T 实现了 Sync,而 &mut T 没有实现,因为可变借用会破坏共享的安全性。
  • 示例:使用 Mutex<T> 来安全地在多线程间共享可变数据。
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..10 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let mut num = data_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Final value: {}", *data.lock().unwrap());
}

这里 Mutex<T> 实现了 SyncSend,允许在多线程间安全共享可变数据。Arc<T> 用于在多个线程间共享 Mutex<T> 的所有权,而 lock 方法获取锁以确保同一时间只有一个线程可以访问数据,从而避免数据竞争。