我是 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);
}
}
我在编译时遇到错误:
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
我不明白指针的生命周期有什么问题以及我应该如何解决这个问题。对我来说,它看起来不错,因为每个线程只获得一个可变的字符串指针,并且不会以任何方式影响向量本身。
Caesar 的答案展示了如何使用 crossbeam 的作用域线程解决问题。如果您不想依赖 crossbeam,那么将值包装在
Arc<Mutex<T>>
中的方法(如 tedtanner 的答案所示)是一个合理的通用策略。
但是在这种情况下,互斥体实际上是不必要的,因为线程不共享字符串,无论是彼此之间还是与主线程。锁定是使用
Arc
的产物,它本身是由静态生命周期强制执行的,而不是共享的需要。尽管锁是无争用的,但它们确实会增加一些开销,最好避免。在这种情况下,我们可以通过将每个字符串移动到其各自的线程,并在线程完成后检索修改后的字符串来避免
Arc
和Mutex
。
此修改仅使用标准库和安全代码进行编译和运行,而不需要
Arc
或 Mutex
:
// ... 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);
}
}
使用
thread::spawn
和 JoinHandle
s,借用检查器不够聪明,无法知道您的线程将在 main
退出之前完成(这对借用检查器有点不公平,它真的无法知道),因此它不能证明 strings
的寿命足以让您的线程在其上工作。您可以通过使用@tedtanner建议的Arc
来回避这个问题(从某种意义上说,这意味着您在运行时进行生命周期管理),或者您可以使用作用域线程。
作用域线程本质上是告诉借用检查器的一种方式:是的,该线程将在该作用域结束(被删除)之前完成。然后,您可以将对当前线程堆栈上的内容的引用传递给另一个线程:
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();
编写此答案时,需要一个用于范围线程的板条箱(此处使用
crossbeam
),但自 1.63 以来,它在 std
中是稳定的。
'static
生命周期(这意味着它们存储在可执行文件中)来防止这种情况发生二进制本身。此外,Rust 不允许您跨线程共享可变引用,因为它是不安全的(多个线程可能会尝试同时更改引用的数据)。您想要将
std::sync::Arc
与
std::sync::Mutex
结合使用。你的
strings
向量将变成
Vec<Arc<Mutex<String>>>
。然后,您可以复制
Arc
(使用
.clone()
)并将其跨线程发送。
Arc
是一个指针,用于保存引用计数,该引用计数以原子方式递增(阅读:以线程安全的方式)。互斥锁允许线程暂时锁定字符串,以便其他线程无法触及它,然后稍后解锁该字符串(线程可以在锁定时安全地更改字符串)。你的代码看起来像这样:
use std::thread;
use std::sync::{Arc, Mutex};
fn child_job(s: Arc<Mutex<String>>) {
// Lock s so other threads can't touch it. It will get
// unlocked when it goes out of scope of this function.
let mut s = s.lock().unwrap();
*s = s.to_uppercase();
}
fn main() {
// initialize
let mut thread_handles = Vec::new();
let strings = vec![
Arc::new(Mutex::new("hello".to_string())),
Arc::new(Mutex::new("world".to_string())),
Arc::new(Mutex::new("testing".to_string())),
Arc::new(Mutex::new("good enough".to_string())),
];
// create threads
for i in 0..strings.len() {
let s = strings[i].clone();
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 {
let s = s.lock().unwrap();
println!("{}", *s);
}
}