将数据移入线程
正确使用 move 闭包。
将数据移入线程 是 CoddyKit 上的免费 Learn Rust Coding 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Learn Rust Coding 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Learn Rust Coding 课程共包含 4 节课。
闭包捕获环境
传给 thread::spawn 的闭包可以使用周围作用域中的变量。默认情况下,Rust 会借用这些变量。
但是,创建的线程可能比创建它的函数存活更久,因此普通借用并不安全。Rust 会在编译时拒绝这种写法。
借用问题
如果线程借用了局部变量,编译器无法证明该变量的存活时间足够长。线程可能在变量被释放后仍继续运行。
下面的代码无法编译,因为闭包只借用了 data。
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// ERROR: closure may outlive `data`
let h = thread::spawn(|| {
println!("{:?}", data);
});
h.join().unwrap();
}move 关键字
在闭包前添加 move,会强制闭包取得所捕获变量的所有权。变量会被移入线程。
现在,线程拥有这些数据,因此可以保证数据在线程的生命周期内始终有效。
use std::thread;
fn main() {
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
println!("{:?}", data);
});
h.join().unwrap();
}所有权转移出去
值被移入线程后,原来的作用域就不能再使用它。所有权已经转移。
移动之后,如果在 main 中尝试使用 data,就会产生编译错误。此时线程是唯一的所有者。
use std::thread;
fn main() {
let data = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", data));
// println!("{:?}", data); // ERROR: value moved
h.join().unwrap();
}移动实现 Copy 的类型
实现了 Copy 的类型(例如整数)会被复制,而不是被移动。闭包会获得它自己的副本。
因此,move 闭包捕获一个数字后,您仍然可以在 main 中使用原来的数字。
use std::thread;
fn main() {
let n = 42;
let h = thread::spawn(move || {
println!("thread sees {}", n);
});
println!("main still sees {}", n);
h.join().unwrap();
}Send:安全转移
只有实现了 Send 特征的类型,才能被移入另一个线程。Send 表示该类型可以安全地跨线程转移所有权。
大多数类型会自动实现 Send。但有少数类型(例如 Rc)不会实现,编译器会拒绝移动它们。
Rc 不实现 Send
Rc 是单线程引用计数器。它的计数没有受到并发更新保护,因此不属于 Send。
尝试将 Rc 移入线程会导致编译失败。在下一课中,您将改用 Arc。
use std::rc::Rc;
use std::thread;
fn main() {
let r = Rc::new(5);
// ERROR: `Rc<i32>` cannot be sent between threads safely
let h = thread::spawn(move || println!("{}", r));
h.join().unwrap();
}移动多个值
一个 move 闭包可以同时捕获多个变量。所有这些变量都会被移入线程。
当工作线程同时需要一些输入和一个标签时,这种方式很方便。
use std::thread;
fn main() {
let label = String::from("sum");
let nums = vec![1, 2, 3, 4];
let h = thread::spawn(move || {
let total: i32 = nums.iter().sum();
println!("{} = {}", label, total);
});
h.join().unwrap();
}移动前先克隆
如果线程和主函数都需要某个值,请先克隆它。将一个副本交给线程,并保留另一个副本。
克隆会复制数据,因此两边各自拥有相互独立的值。
use std::thread;
fn main() {
let original = String::from("hello");
let for_thread = original.clone();
let h = thread::spawn(move || println!("thread: {}", for_thread));
println!("main: {}", original);
h.join().unwrap();
}将移动的数据返回
拥有已移动数据的线程可以返回这些数据,通过 join 将所有权交还给父线程。
这种模式会先移入数据,处理数据,再将结果移出。
use std::thread;
fn main() {
let mut v = vec![3, 1, 2];
let h = thread::spawn(move || {
v.sort();
v
});
let sorted = h.join().unwrap();
println!("{:?}", sorted);
}为什么必须使用 move
没有 move 时,借用检查器会假定闭包使用借用。由于创建的线程没有与调用者绑定的固定生命周期,借用是不安全的。
move 关键字会将借用转换为所有权转移,从而满足 spawn 的 'static 要求。
快速检查
检验您对将数据移入线程的理解。
回顾
您已经了解到,线程闭包必须拥有其使用的数据,这是通过 move 关键字实现的。
移动会转移所有权,而 Copy 类型会被复制。Send trait 标记了可以安全转移的内容,而 Rc 不实现 Send。
接下来,您将使用 Arc 和 Mutex 在线程之间共享数据。
常见问题解答
「将数据移入线程」课时是免费的吗?
是的 — 「将数据移入线程」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Learn Rust Coding 课程的其余内容,请升级到 CoddyKit PRO。 Learn Rust Coding 课程共包含 4 节课。
「将数据移入线程」这节课中我会学到什么?
正确使用 move 闭包。 你通过在浏览器中直接运行的动手代码来练习 Learn Rust Coding,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Learn Rust Coding 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Learn Rust Coding 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「将数据移入线程」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Learn Rust Coding 课中编写并运行代码吗?
能。每节 Learn Rust Coding 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。