当被线程使用时,借用值的生命周期不够长。

4

我正在追求我的Rust之旅(非常喜欢),并且我正在探索线程。像往常一样,我遇到了一个我不理解的错误。

这是一个最简示例

use std::thread;

pub fn compute_something(input: &Vec<&usize>) -> usize {
    input.iter().map(|v| *v).sum()
}

pub fn main() {
    let items = vec![0, 1, 2, 3, 4, 5];
    
    let mut slice: Vec<&usize> = Vec::new();

    slice.push(&items[1]); // borrowed value does not live long enough
    // argument requires that `items` is borrowed for `'static`
    slice.push(&items[2]); // borrowed value does not live long enough
    // argument requires that `items` is borrowed for `'static`

    assert_eq!(3, compute_something(&slice));
    
    let h = thread::spawn(move || compute_something(&slice));
    
    match h.join() {
        Ok(result) => println!("Result: {:?}", result),
        Err(e) => println!("Nope: {:?}", e)
    }
} // `items` dropped here while still borrowed

当然我做了一个playground来说明。

如果我删除线程部分(即assert_eq!行后面的所有内容),只调用compute_something(&slice),则可以编译通过。

以下是我不明白的三个主要问题:

  • 为什么在程序末尾借用items时删除它会有问题?运行时不应该能够很好地清理内存吗?毕竟我无法在main之外访问slice

  • 还有哪些部分会在程序结束时借用items?是slice吗?如果是这样,为什么删除assert_eq!行之后同样的程序就可以编译通过?我看不出它如何改变借用模式。

  • 为什么从线程的闭包中调用compute_something会创建问题,我该如何解决?


1
这个回答解决了你的问题吗?如何将对栈变量的引用传递给线程? - Herohtar
2个回答

3
你将 slice 移动到传递给 thread::spawn() 的闭包内。由于传递给 thread::spawn() 的闭包必须是 'static,这意味着移动到闭包内的向量也不能借用任何不是 'static 的东西。因此编译器推断出 slice 的类型为 Vec<&'static usize>
但它确实借用了一个不是 'static 的东西——你试图将其推入其中的值从局部变量中借用,因此编译器会对此发出警告。
修复此问题的最简单方法是将 slice 设为 Vec<usize>,并完全不从 items 中借用。
另一种选择是使用 crossbeam crate 的 scoped threads , 它们知道如何安全地从本地变量中借用,通过强制在作用域结束之前加入所有线程来实现。
直接回答你提出的问题:
为什么在程序结束时借用的项目丢失会有问题,运行时不应该清理内存吗?
main() 终止时,所有线程也将终止 - 但是,在线程终止之前,main() 中的本地值已被销毁,这期间可能存在悬空引用,这违反了 Rust 的内存安全模型。这就是为什么 thread::spawn() 需要一个 'static 闭包的原因。
即使您自己加入线程,借用检查器也不知道加入线程是否结束了借用。(这就是 crossbeam 的作用。)
仍然在程序结束时借用 items 的是什么?
移动到闭包中的向量仍在借用 items
为什么从线程的闭包中调用 compute_something 会创建问题,我该如何解决它?
调用此函数并未创建问题。将slice移入闭包中才会导致问题。

谢谢,我会尝试使用Crossbeam!我认为我错过的关键概念是借用检查器不理解加入线程会结束借用,难道不应该吗?还是我漏掉了什么? - djfm
@djfm 这将需要在借用检查器中明确制定一个特殊情况。在Rust中没有语法可以表示“调用此方法会结束其他某些借用,但是不调用此方法不会结束这些借用”。 - cdhowie
请注意,您可以使用unsafe块使其工作,其中可以将引用强制转换为具有不同生命周期的引用。这就是crossbeam在幕后的工作原理--即使它们捕获本地变量,它也会将闭包的生命周期转换为'static,因为crossbeam保证线程在返回控制权到外部函数之前结束,其中拥有的值可能被销毁。换句话说,它在幕后使用unsafe,但公开了一个_安全_接口(这也是Rust本身所做的)。 - cdhowie
@djfm 所以你可以自己做这个,但是那样的话你就需要负责维护内存安全保证。在我看来,更好的选择是依靠crossbeam来为你进行检查。 - cdhowie

0

这是我解决这个问题的方法。
我使用了 Box::Leak: https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak

let boxed_data = data.into_boxed_slice();
let boxed_data_static_ref = Box::leak(boxed_data);

let compressed_data = &boxed_data_static_ref[start_of_data..start_of_data+zfr.compressed_size as usize];


let handles = (0..NUM_THREADS).map(|thread_nr| {
    thread::spawn(move || {
        main2(thread_nr, compressed_data, zfr.crc);
    })
}).collect::<Vec<_>>();

for h in handles {
    h.join().unwrap();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接