当使用std::thread并发时,我对如何管理对对象的引用的生存期感到困惑.

我想我理解I/Rust想要解决的核心问题,那就是如果我向一个函数传递对一个对象的引用,并且该函数产生使用该引用的线程,我需要确保原始数据位将至少与我产生的线程一样长.我读了Lukas Kalbertodt对this question的回答,我知道我可以用std::thread::scope来做到这一点.我附上了一个简单的例子,如下:

use std::thread;

fn main() {
    let owned_string: String = String::from("OWNED DATA");
    foo(&owned_string);
}

fn foo(data: &str) {
    thread::scope( |s| {
        s.spawn(move || {
            println!("I am doing something in the first thread!");
            println!("I have the data, look: {}", &data);
        });

        s.spawn(move || {
            println!("I am doing something in the second thread!");
            println!("I have the data, too, look: {}", &data);
        });
    });
}

这将打印如下内容

I am doing something in the first thread!
I have the data, look: OWNED DATA
I am doing something in the second thread!
I have the data, too, look: OWNED DATA

需要说明的是,这一切都是可行的,因为使用thread::scope意味着线程中引用的使用完全包含在定义的作用域内.当作用域结束时,即函数返回之前,引用将被删除(我认为).在函数返回之前,数据本身不能被删除,所以总的来说,程序是内存安全的,因为没有办法让引用挂起.

相比之下,如果没有thread::scope,但线程只是生成的,那么函数可能会在线程结束之前以某种方式返回,因此引用的数据可能会超出范围,导致引用悬而未决.

我不明白的是,为什么我不能在函数中使用handle.join()来实现相同的功能.例如:

fn bar(data: &str) {
    let mut handles = Vec::new();
    handles.push(thread::spawn(move || {
        println!("I am doing something in the first thread!");
        println!("I have the data, look: {}", &data);
    }));

    handles.push(thread::spawn(move || {
        println!("I am doing something in the second thread!");
        println!("I have the data, too, look: {}", &data);
    }));

    for handle in handles {
        handle.join();
    }
}

这不能编译,编译器告诉我:

error[E0521]: borrowed data escapes outside of function
  --> src/main.rs:24:18
   |
22 |   fn bar(data: &str) {
   |          ----  - let's call the lifetime of this reference `'1`
   |          |
   |          `data` is a reference that is only valid in the function body
23 |       let mut handles = Vec::new();
24 |       handles.push(thread::spawn(move || {
   |  __________________^
25 | |         println!("I am doing something in the first thread!");
26 | |         println!("I have the data, look: {}", &data);
27 | |     }));
   | |      ^
   | |      |
   | |______`data` escapes the function body here
   |        argument requires that `'1` must outlive `'static`

For more information about this error, try `rustc --explain E0521`.

原则上,我明白它在说什么(我想),但我也明白,当我调用循环对句柄执行handle.join()时,它会阻塞,直到所有线程都完成.因此,在所有线程都完成并且引用可以安全删除之前,函数不可能返回.

有人能解释一下为什么第二个例子不是内存安全的吗?或者,这是Rust编译器过于保守的一个例子吗?

推荐答案

就像这种一般性/宽泛的设计决策一样,没有明显的答案:是的,它本可以以其他方式设计,以使这样或那样的用例受益;但这将损害这样或那样的系统上的这样或那样的用例……

以下是人们需要考虑的几件事:

  • 底层操作系统没有义务实际执行这些线程中的任何一个(除非它会导致死锁),即使在从spawn()返回之后也是如此.这包括在主线程退出之前不执行"子线程".
  • 根本没有什么强迫您调用.join(),编译器也不能证明if random() == 42 { my_thread.join() } 将执行的一般情况.
  • join()被调用之前,生成线程可能是panic!(),所以根本不会调用它.但是"子线程"将会执行(见第一点)
  • 如果你joined()一些线程,但其他人只有当is_np_equal_to_p()False?编译器不能静态地证明每个执行路径都是正确的,除非它能证明is_np_equal_to_p()的结果.

Rust相关问答推荐

使用nom将任何空白、制表符、白线等序列替换为单个空白

rust 迹-内存管理-POP所有权-链表

函数内模块的父作用域的访问类型

有没有一种方法可以创建一个闭包来计算Rust中具有随机系数的n次多项式?

如何将带有嵌套borrow /NLL 的 Rust 代码提取到函数中

Windows 上 ndarray-linalg 与 mkl-stats 的链接时间错误

是否可以在不直接重复的情况下为许多特定类型实现一个函数?

Rust:为什么 &str 不使用 Into

`UnsafeCell` 在没有锁定的情况下跨线程共享 - 这可能会导致 UB,对吗?

将引用移动到线程中

unwrap 选项类型出现错误:无法移出共享引用后面的*foo

Rust编译器通过哪些规则来确保锁被释放?

从嵌入式 Rust 中的某个时刻开始经过的时间

如何限制通用 const 参数中允许的值?

如果不满足条件,如何在 Rust 中引发错误

`use std::error::Error` 声明中断编译

在空表达式语句中移动的值

如何在 nom 中构建负前瞻解析器?

Rust 跨同一文件夹中文件的可见性

为什么这个闭包没有比 var 长寿?