Rust编程与项目实战-模块std::thread(之二)
《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
Rust编程与项目实战-模块std::thread(之一)-CSDN博客
12.3.3 在线程中使用其他线程数据
不和主线程交互的子线程是非常少见的。Rust语言和其他语言不一样的地方是,如果线程中直接使用了其他线程定义的数据,则会报错。这里所说的外部变量就是其他线程中定义的变量。比如下面的代码,子线程中直接使用主线程定义的字符串就会报错:
use std::thread;
fn main() {
let data = String::from("hello world");
let thread = std::thread::spawn(||{
println!("{}", data);
});
thread.join();
}
如果编译就会报错:
error[E0373]: closure may outlive the current function, but it borrows `data`, which is owned by the current function
线程中使用了其他线程的变量是不合法的,必须使用move表明线程拥有data的所有权,我们可以使用move关键字把data的所有权转到子线程内,代码如下:
use std::thread;
fn main() {
let data = String::from("hello world");
let thread = std::thread::spawn(move ||{//使用move 把data的所有权转到线程内
println!("{}", data);
});
thread.join();
}
这个时候,就能正确输出结果“hello world”了。
move闭包通常和thread::spawn函数一起使用,它允许用户使用其他线程的数据,这样在创建新线程时,可以把其他线程中的变量的所有权从一个线程转移到另一个线程,然后就可以使用该变量了。下面来看一个实例,一个常见的应用模式是使用多线程访问列表中的元素来执行某些运算。
【例12.5】 多个子线程使用主线程中的数据
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::thread;
fn main() {
let v = vec![1, 3, 5, 7, 9];
let mut childs = vec![];
for n in v {
let c = thread::spawn(move || {
println!("{}", n * n);
});
childs.push(c);
};
for c in childs { //等待所有子线程结束
c.join().unwrap();
}
}
这里的move是必要的,否则没法保证主线程中的v会不会在子线程结束之前被销毁。使用move之后,所有权转移到了子线程内,从而使得不会出现因为生命周期造成的数据无效的情况。子线程的执行周期可能比主线程还长。因此,很可能出现结果还没有完全打印出来,就已经结束的情况。为了防止这个情况,我们存下每个线程句柄,并在最后使用 join 阻塞主线程。
保存文件并运行,运行结果如下:
1
9
25
49
81
12.3.4 线程函数
现在我们知道了,thread::spawn 函数接受一个闭包作为参数,闭包中的代码会在子线程中执行,比如:
let handle = thread::spawn(|| {
//子线程执行的代码
});
但如果子线程执行的代码比较长,我们通过会另外写一个函数来封装这些代码,这样在thread::spawn中只需写一个函数调用即可,这个在新线程中执行的函数通常称为线程函数。下面来看一个实例,我们设计了一个线程函数,并且它是一个递归函数,为了模拟长时间运行。
【例12.6】 使用一个递归的线程函数
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::thread;
fn fibonacci(n: u32) -> u32 { //这个函数是线程函数,并且是一个递归函数,用于求斐波那契数列
if n == 0 {
return 0;
} else if n == 1 {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
fn main() {
let child = thread::spawn(|| {
let f = fibonacci(30); //调用线程函数
println!("Hello from a thread!. fibonacci(30) = {}", f); //打印数列结果
f //返回子线程结果,这里也就是函数fibonacci的返回值
});
println!("Hello, world!"); //主线程中执行的代码
let v = child.join().expect("Could not join child thread"); //等待子线程结束,并得到子线程结果
println!("value: {:?}", v);
}
我们把子线程中要执行的代码(这里是求斐波那契数列)单独放在一个函数fibonacci中,这样在thread::spawn函数中只需要调用该函数(fibonacci(30);)即可,这样代码简洁多了,而且方便模块化开发,比如可以让算法工程师专门实现斐波那契数列函数,而其他程序员只需要调用即可,这样可以做到并行开发,提高了效率。斐波那契数列函数执行时间较长,主线程末尾一定要等待子线程结束,也就是调用join函数,join返回一个Result,可以使用 expect 方法来获取返回值。这样主线程就会等待子线程完成,而不会先结束程序,这样就可以看到我们想要的结果。
保存文件并运行,运行结果如下:
Hello, world!
Hello from a thread!. fibonacci(30) = 832040
value: 832040
12.3.5 available_parallelism返回默认并行度
available_parallelism函数返回程序应使用的默认并行度的估计值。该函数声明如下:
pub fn available_parallelism() -> Result<NonZeroUsize>
并行性是一种资源,一台给定的机器提供了一定的并行能力,即它可以同时执行的计算数量的限制。这个数字通常对应CPU或计算机的数量,但在各种情况下可能会有所不同。诸如VM或容器编排器之类的主机环境可能希望限制其中的程序可用的并行量。这样做通常是为了限制(无意中)resource-intensive程序对同一台机器上运行的其他程序的潜在影响。
提供此函数的目的是提供一种简单且可移植的方式来查询程序应使用的默认并行度。它不公开有关NUMA区域的信息,不考虑(协)处理器能力的差异,并且不会修改程序的全局状态以更准确地查询可用并行度的数量。资源限制可以在程序运行期间更改,因此不会缓存该值,而是在每次调用此函数时重新计算,不应从热代码中调用它。
此函数返回的值应被视为在任何给定时间可用的实际并行量的简化近似值。要更详细或更准确地了解程序可用的并行量,用户可能还希望使用特定平台的API。以下平台限制当前适用于available_parallelism。
(1)在Windows上:它可能低估了具有超过64个逻辑CPU的系统上可用的并行量。但是,程序通常需要特定的支持才能利用超过64个逻辑CPU,并且在没有此类支持的情况下,此函数返回的数字准确地反映了程序默认可以使用的逻辑CPU的数量。它可能会高估受process-wide关联掩码或作业对象限制的系统上可用的并行量。
(2)在Linux上:当受process-wide关联掩码限制或受cgroup限制影响时,它可能会高估可用的并行量。
在具有CPU使用限制的VM(例如过度使用的主机)中运行时,可能会高估可用的并行量。此函数将在以下情况下(但不限于)返回错误:如果目标平台的并行量未知,或者程序没有权限查询可用的并行量。
【例12.7】 得到当前系统的默认并行度
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::{io, thread};
fn main() -> io::Result<()> {
let count = thread::available_parallelism()?.get();
assert!(count >= 1_usize);
println!("{},{}",count,1_usize);
Ok(())
}
1_usize的值就是1。我们把得到的默认并行度存于count中,如果count小于1,那就抛出异常,否则打印结果。
保存文件并运行,运行结果如下:
2,1
可见,当前系统的默认并行度是2。
12.3.6 获得当前线程的名称和id
当前线程的属性包括线程id、名称。获取它们的函数被定义在std::thread:: Thread这个结构体中,所以我们首先要获取这个结构体,也就是获取当前线程的Thread结构体,这个函数是current,声明如下:
pub fn current() -> Thread
Thread其实是std::thread的一个私有结构(Struct),这个结构体对外提供了一些函数,以此来获取线程的id、名称等属性。
有了当前线程的Thread结构,就可以得到名称,该函数声明如下:
pub fn name(&self) -> Option<&str>
该函数返回字符串。
【例12.8】 获取和设置线程名称
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;
fn main() {
let thr0 = thread::current(); //获取当前线程的Thread结构
let thread_name = thr0.name().unwrap_or("unknown"); //获取当前线程的名称
println!("当前线程的名称:{}", thread_name); //打印输出名称
}
unwrap_or是用于从Result对象中获取值的宏。当Result对象是Ok时,两者都会返回Ok中的值。但是当Result对象是Err时,unwrap_or将返回一个默认值。这个默认值是宏的参数,在调用unwrap_or时就已经确定了。所以,当你想要在Result对象是Err时使用固定的默认值时,就可以使用unwrap_or。
保存文件并运行,运行结果如下:
当前线程的名称:main
下面再看获得当前线程的id,线程id用于区分不同的线程,id号是唯一的。在Rust中,获得当前线程id的函数声明如下:
pub fn id(&self) -> ThreadId
该函数的返回值是一个结构体ThreadId,该结构体的大小是8字节。ThreadId是一个不透明的对象,它唯一地标识在进程生存期内创建的每个线程。线程ID保证不会被重用,即使在线程终止时也是如此。ThreadId受Rust的标准库控制,ThreadId和底层平台的线程标识符概念之间可能没有任何关系。因此,这两个概念不能互换使用。ThreadId可以从结构体Thread上的id函数中得到结果。
【例12.9】 得到线程id
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;
fn main() {
let child_thread = thread::spawn(|| { //创建线程
thread::current().id() //在子线程中执行的代码得到的就是当前子线程的id
});
let child_thread_id = child_thread.join().unwrap();//得到子线程的id
assert!(thread::current().id() != child_thread_id);//如果表达式为假,则触发断言
println!("thread::current().id()={:?},child_thread_id={:?}", thread::current().id(),child_thread_id);
}
在代码中,我们不仅得到了当前线程id,还创建了另一个线程,并且比较了这两个线程id。assert!宏用于检查一个表达式是否为真(true),如果表达式为假(false),则会触发断言,程序会终止运行。
保存文件并运行,运行结果如下:
thread::current().id()=ThreadId(1),child_thread_id=ThreadId(2)
可见,主线程和子线程的id不一样。
原文地址:https://blog.csdn.net/brucexia/article/details/144018570
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!