0Pricing
Learn Rust Coding · 课时

作用域线程

跨线程借用

作用域线程 是 CoddyKit 上的免费 Learn Rust Coding 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Learn Rust Coding 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Learn Rust Coding 课程共包含 4 节课。

在线程中借用的问题

普通的 thread::spawn 闭包必须满足 'static:它不能借用局部变量,因为线程的存活时间可能超过拥有这些变量的函数。这就是您经常会看到 move 和 Arc 的原因。

作用域线程通过保证每个线程都会在作用域结束前完成工作来解决这个问题,因此借用局部数据是安全的。

为什么 spawn 需要 'static

使用 thread::spawn 时,生成的线程可能会在 main 或任意函数返回后继续运行。如果它借用了局部变量,那么在线程仍在使用该变量时,变量可能已经被销毁。Rust 会在编译时禁止这种情况,迫使您将拥有的数据移入闭包。

use std::thread;

fn main() {
    let nums = vec![1, 2, 3];
    // move transfers ownership into the thread
    let handle = thread::spawn(move || {
        println!("in thread: {:?}", nums);
    });
    handle.join().unwrap();
}

介绍 thread::scope

std::thread::scope 在 Rust 1.63 中稳定,它会创建一个允许线程借用局部变量的作用域。该作用域会阻塞,直到其中的所有线程都完成,因此借用永远不会悬空。

您可以使用作用域句柄 s 调用 s.spawn(...) 来生成线程,而不是调用 thread::spawn。

use std::thread;

fn main() {
    let data = vec![10, 20, 30];
    thread::scope(|s| {
        s.spawn(|| {
            println!("borrowed: {:?}", data);
        });
    });
    // data is still usable here
    println!("after scope: {:?}", data);
}

无需 move 即可借用

在 thread::scope 中,您无需使用 move,即可通过引用读取局部变量。多个作用域线程可以同时共享同一份数据的不可变借用,就像普通引用一样。

use std::thread;

fn main() {
    let message = String::from("shared text");
    thread::scope(|s| {
        s.spawn(|| println!("thread 1 sees: {}", message));
        s.spawn(|| println!("thread 2 sees: {}", message));
    });
    println!("main still owns: {}", message);
}

跨切片分配工作

一种常见模式是切分一个切片,让每个线程处理其中的一部分。作用域线程使这一过程更加简洁,因为每个线程都可以直接借用原始切片的一部分,无需克隆。

use std::thread;

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];
    let (left, right) = numbers.split_at(3);
    thread::scope(|s| {
        s.spawn(|| {
            let sum: i32 = left.iter().sum();
            println!("left sum:  {}", sum);
        });
        s.spawn(|| {
            let sum: i32 = right.iter().sum();
            println!("right sum: {}", sum);
        });
    });
}

收集返回值

与普通线程一样,s.spawn 会返回一个 ScopedJoinHandle。调用 .join() 即可获取线程的返回值。您可以收集各个句柄,并在生成线程后统一加入它们,以收集结果。

use std::thread;

fn main() {
    let inputs = [2, 4, 6];
    let mut handles = vec![];
    thread::scope(|s| {
        for &x in &inputs {
            handles.push(s.spawn(move || x * x));
        }
        let results: Vec<i32> = handles.into_iter()
            .map(|h| h.join().unwrap())
            .collect();
        println!("{:?}", results);
    });
}

可变借用需要谨慎处理

两个作用域线程不能同时持有同一份数据的可变借用,否则会违反 Rust 的别名规则。要从多个线程修改共享数据,您仍然需要使用 Mutex;不过,单个线程可以对互不重叠的部分进行唯一的可变借用。

下面的示例中,每个线程都通过 split_at_mut 修改数组中不同的一半。

use std::thread;

fn main() {
    let mut data = [1, 2, 3, 4];
    let (a, b) = data.split_at_mut(2);
    thread::scope(|s| {
        s.spawn(|| { for x in a.iter_mut() { *x *= 10; } });
        s.spawn(|| { for x in b.iter_mut() { *x += 100; } });
    });
    println!("{:?}", data);
}

作用域会自动加入线程

您不必对每个作用域线程都调用 join。当 scope 闭包返回时,Rust 会在继续执行之前自动加入所有尚未加入的线程。这就是借用能够在线程的整个生命周期内保持有效的原因。

use std::thread;
use std::time::Duration;

fn main() {
    let label = String::from("task");
    thread::scope(|s| {
        s.spawn(|| {
            thread::sleep(Duration::from_millis(30));
            println!("{} done", label);
        });
        println!("spawned, scope will wait");
    });
    println!("all scoped threads finished");
}

将作用域与共享修改结合使用

当线程必须修改同一个值时,可以将作用域线程与 Mutex 结合使用。由于作用域已经允许线程直接借用局部的 Mutex,因此不需要使用 Arc。

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    thread::scope(|s| {
        for _ in 0..5 {
            s.spawn(|| {
                let mut n = counter.lock().unwrap();
                *n += 1;
            });
        }
    });
    println!("counter = {}", *counter.lock().unwrap());
}

作用域线程与生成线程:如何选择

当工作量有明确边界、会在某个函数内完成,并且您希望在不使用 Arc 或克隆的情况下借用栈数据时,请使用作用域线程。

当线程必须超过当前函数继续运行,或需要在整个程序生命周期内运行时,请使用thread::spawn。作用域线程不能离开其作用域。

并行映射示例

综合运用这些知识:创建一个简短的并行映射程序,让每个线程在借用输入的同时转换向量中的一个元素,然后按顺序收集结果。

use std::thread;

fn parallel_double(items: &[i32]) -> Vec<i32> {
    let mut handles = Vec::new();
    let mut out = Vec::new();
    thread::scope(|s| {
        for &x in items {
            handles.push(s.spawn(move || x * 2));
        }
        for h in handles {
            out.push(h.join().unwrap());
        }
    });
    out
}

fn main() {
    let nums = vec![1, 2, 3, 4];
    println!("{:?}", parallel_double(&nums));
}

快速检查

测试您对作用域线程的理解。

回顾

您学习了作用域线程:

  • thread::spawn 要求闭包满足 'static;作用域线程不要求。
  • thread::scope 让线程能够安全地借用局部变量。
  • 作用域会在返回前自动加入所有线程。
  • 共享修改仍然需要 Mutex,但在作用域内不需要 Arc。
  • 对于有明确边界、局部于函数的并行任务,请使用作用域线程。

常见问题解答

「作用域线程」课时是免费的吗?

是的 — 「作用域线程」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Learn Rust Coding 课程的其余内容,请升级到 CoddyKit PRO。 Learn Rust Coding 课程共包含 4 节课。

「作用域线程」这节课中我会学到什么?

跨线程借用 你通过在浏览器中直接运行的动手代码来练习 Learn Rust Coding,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Learn Rust Coding 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Learn Rust Coding 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「作用域线程」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Learn Rust Coding 课中编写并运行代码吗?

能。每节 Learn Rust Coding 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. mpsc 通道
  2. 使用 Arc/Mutex 共享状态
  3. 作用域线程
  4. Crossbeam 通道
← 返回 Learn Rust Coding