Rust学习笔记RustRust多线程 - 限定作用域的线程(Scoped Thread)
xvanzai相关概念
什么是限定作用域的线程?
- 定义:使用std::thread::scoped创建的线程,生命周期受限于特定作用域
- 特性:线程在作用域结束前必须终止,无需手动管理JoinHandle
主要优点
- 简化线程管理:
- 无需手动调用**join()**,作用域自动确保线程退出。
- 减少管理线程生命周期的复杂性。
- 安全的数据访问:
- 线程可以安全引用作用域内的本地数据,无需复制或克隆(如使用Arc或clone)。
- 编译器保证数据在作用域内有效,限制所有权的可能性。
- 简化工作流:
- 闭包可以直接访问本地变量,编写线程函数更直观。
- 提高代码可读性和维护性。
局限性
- 线程生命周期受限
- 你不能在一个作用域中创建一个线程并期望它永远运行。
- 强制终止
使用作用域线程
普通线程使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| use std::{thread, time::Duration};
fn main() { let mut handles = Vec::new();
for i in 0..5 { let handle = thread::spawn(move || { thread::sleep(Duration::from_secs(1)); println!("Normal thread: {}", i); });
handles.push(handle); }
handles.into_iter().for_each(|handle| { handle.join().unwrap(); }); }
|
上面的代码创建了一个线程数组,包含了5个线程。在线程中,我们要想打印i,需要加上move关键字。并且我们需要手动处理**handle.join().unwrap()**,不然在线程执行前主线程就退出了,得不到任何输出。以上代码输出:
1 2 3 4 5
| Normal thread: 1 Normal thread: 2 Normal thread: 0 Normal thread: 3 Normal thread: 4
|
作用域线程使用方式
1 2 3 4 5 6 7 8 9 10 11 12
| use std::{thread, time::Duration};
fn main() { thread::scope(|s| { for i in 0..5 { s.spawn(move || { thread::sleep(Duration::from_secs(1)); println!("Normal thread: {}", i); }); } }); }
|
输出:
1 2 3 4 5
| Normal thread: 2 Normal thread: 3 Normal thread: 4 Normal thread: 1 Normal thread: 0
|
可以看到,我们并没有使用handle.join().unwrap(),也输出了想要的结果。在代码中,使用的是s.spawn新建线程,而不是thread::spawn创建线程,当 scope 闭包执行结束时,Rust 会自动确保所有这些子线程都已完成,我们不再需要手动管理 JoinHandle。
1 2 3 4 5 6 7 8 9 10 11 12 13
| use std::{thread, time::Duration};
fn main() { let a = String::from("hello"); thread::scope(|s| { for _ in 0..5 { s.spawn(|| { thread::sleep(Duration::from_secs(1)); println!("Normal thread: {}", a); }); } }); }
|
修改代码如上,cargo run。可以发现我们可以在线程中直接借用 a ,而不用使用 move 关键字。
可以看到,作用域线程可以安全地借用外部变量。在传统 thread::spawn 中,由于线程可能比创建它的函数活得更久,所以闭包必须获得变量的完整所有权(通过 move),且变量必须是 ‘static 的。但在这里,s.spawn 的闭包可以直接借用 main 函数中的变量 a,并且不需要 move 关键字!这是因为编译器知道,scope 会确保所有子线程在 a 被销毁前就已结束,因此借用是完全安全的。
下面代码中解释了有关生命周期的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| use std::{thread, time::Duration};
fn main() {
thread::scope(|s| { for i in 0..5 { s.spawn(move || { thread::sleep(Duration::from_secs(1)); println!("Scoped thread: {i}"); }); } });
}
|
‘scope 代表了作用域线程可以存活的范围,而 ‘env 代表了被线程借用的外部环境(如变量 a)的生命周期。Rust 编译器会强制要求 ‘env 必须比 ‘scope 更长,从而在编译期就杜绝了悬垂指针的风险。
作用域线程使用案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| use std::thread;
fn main() { const CHUNK_SIZE: usize = 10; let numbers: Vec<u32> = (1..10000).collect(); let chunks = numbers.chunks(CHUNK_SIZE);
let total_sum = thread::scope(|s| { let mut handles = Vec::new();
for chunk in chunks { let handle = s.spawn(move || chunk.iter().sum::<u32>()); handles.push(handle); }
handles.into_iter().map(|h| h.join().unwrap()).sum::<u32>() });
println!("Total sum: {total_sum}"); }
|
如果不用作用域线程,代码可能会这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| use std::thread;
fn main() { const CHUNK_SIZE: usize = 10; let numbers: Vec<u32> = (1..10000).collect(); let chunked_data: Vec<Vec<u32>> = numbers .chunks(CHUNK_SIZE) .map(|chunk| chunk.to_vec()) .collect();
let mut handles = Vec::new();
for chunk in chunked_data { let handle = thread::spawn(move || chunk.iter().sum::<u32>()); handles.push(handle); }
let total_sum: u32 = handles .into_iter() .map(|h| h.join().unwrap()) .sum();
println!("Total sum: {total_sum}"); }
|
这是个将数组分片计算综合的示例,在使用作用域线程的代码中,不用进行 to_vec() 创建数据副本的操作,因为 numbers 的生命周期持续到 main 函数结束, chunks 的生命周期跟随 numbers 到 main 函数结束,所以在作用域线程中可以直接转移 chunk 所有权去借用 numbers 中的数据。而在普通的线程中,无法进行该操作,普通线程中要求变量为 ‘static 生命周期才能直接借用,所以只能通过 to_vec() 转换成拥有所有权的数据,再将其所有权转入线程中。可以看到,作用域线程的处理方式更加优雅,理论上性能也更好,因为免去了拷贝操作。