在 Rust 中使用多线程更改向量中的元素

vse*_*eto 5 multithreading vector mutable rust

我是 Rust 新手,我正在尝试将计算工作分配给线程。

我有字符串向量,我想为每个字符串创建一个线程来完成他的工作。有简单的代码:

use std::thread;

fn child_job(s: &mut String) {
    *s = s.to_uppercase();
}

fn main() {
    // initialize
    let mut thread_handles = vec![];
    let mut strings = vec![
        "hello".to_string(),
        "world".to_string(),
        "testing".to_string(),
        "good enough".to_string(),
    ];

    // create threads
    for s in &mut strings {
        thread_handles.push(thread::spawn(|| child_job(s)));
    }

    // wait for threads
    for handle in thread_handles {
        handle.join().unwrap();
    }

    // print result
    for s in strings {
        println!("{}", s);
    }
}
Run Code Online (Sandbox Code Playgroud)

我在编译时遇到错误:

error[E0597]: `strings` does not live long enough
  --> src/main.rs:18:14
   |
18 |     for s in &mut strings {
   |              ^^^^^^^^^^^^
   |              |
   |              borrowed value does not live long enough
   |              argument requires that `strings` is borrowed for `'static`
...
31 | }
   | - `strings` dropped here while still borrowed

error[E0505]: cannot move out of `strings` because it is borrowed
  --> src/main.rs:28:14
   |
18 |     for s in &mut strings {
   |              ------------
   |              |
   |              borrow of `strings` occurs here
   |              argument requires that `strings` is borrowed for `'static`
...
28 |     for s in strings {
   |              ^^^^^^^ move out of `strings` occurs here
Run Code Online (Sandbox Code Playgroud)

我不明白指针的生命周期有什么问题以及我应该如何解决这个问题。对我来说,它看起来不错,因为每个线程只获得一个可变的字符串指针,并且不会以任何方式影响向量本身。

use*_*342 6

Caesar 的答案展示了如何使用 crossbeam 的作用域线程解决问题。如果您不想依赖 crossbeam,那么将值包装在Arc<Mutex<T>>如 tedtanner 的答案所示)是一种合理的一般策略。

然而,在这种情况下,互斥体实际上是不必要的,因为线程不共享字符串,无论是彼此之间还是与主线程。锁定是 using 的一个产物Arc,它本身是由静态生命周期强制执行的,而不是共享的需要。尽管锁是无争用的,但它们确实会增加一些开销,最好避免。在这种情况下,我们可以通过移动Arc来避免两者Mutex到其各自的线程,并在线程完成后检索修改后的字符串来

此修改仅使用标准库和安全代码进行编译和运行,并且不需要ArcMutex

// ... child_job defined as in the question ...

fn main() {
    let strings = vec![
        "hello".to_string(),
        "world".to_string(),
        "testing".to_string(),
        "good enough".to_string(),
    ];

    // start the threads, giving them the strings
    let mut thread_handles = vec![];
    for mut s in strings {
        thread_handles.push(thread::spawn(move || {
            child_job(&mut s);
            s
        }));
    }

    // wait for threads and re-populate `strings`
    let strings = thread_handles.into_iter().map(|h| h.join().unwrap());

    // print result
    for s in strings {
        println!("{}", s);
    }
}
Run Code Online (Sandbox Code Playgroud)

操场


Cae*_*sar 5

使用thread::spawnand JoinHandles,借用检查器不够聪明,无法知道您的线程将在main退出之前完成(这对借用检查器有点不公平,它真的无法知道),因此它无法证明该线程strings将存活足够长的时间让你的线程可以处理它。您可以通过使用Arc@tedtanner 建议的 s 来回避这个问题(从某种意义上说,这意味着您在运行时进行生命周期管理),或者您可以使用作用域线程。

作用域线程本质上是告诉借用检查器的一种方式:是的,该线程将在该作用域结束(被删除)之前完成。然后,您可以将对当前线程堆栈上的内容的引用传递给另一个线程:

crossbeam::thread::scope(|scope| {
    for s in &mut strings {
        scope.spawn(|_| child_job(s));
    }
}) // All spawned threads are auto-joined here, no need for join_handles
.unwrap();
Run Code Online (Sandbox Code Playgroud)

操场

当编写这个答案时,需要一个用于范围线程的板条箱(crossbeam此处使用),但这从 1.63 开始就稳定了std

  • “当程序退出时,它只会让操作系统清理这些线程” - 但不能保证程序在“main”末尾退出,因为它可以作为常规函数调用,而不仅仅是作为条目观点。此外,在“main”之后需要运行一些代码,这将使未连接的线程保持更多的活动状态。 (2认同)