0Pricing
Learn Rust Coding · 课时

将数据移入线程

正确使用 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 创建线程
  2. 将数据移入线程
  3. 使用 Arc 和 Mutex 共享数据
  4. 加入线程并收集结果
← 返回 Learn Rust Coding