Rust编程与项目实战-模块std::thread(之一)

【图书介绍】《Rust编程与项目实战》-CSDN博客

《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

Rust编程与项目实战_夏天又到了的博客-CSDN博客

12.3.1 spawn创建线程

在Rust中,我们可以使用std::thread::spawn函数来创建一个新的线程,也称派生线程。该函数声明如下:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>

F: FnOnce() -> T + Send + 'static,

T: Send + 'static,

参数f是一个闭包(Closure),是线程要执行的代码。spawn函数生成一个新线程,并返回JoinHandle(连接句柄),连接句柄提供了一个join方法,可用于连接派生的线程。如果派生的线程崩溃,join将返回一个错误信息。

如果删除连接句柄(JoinHandle),则派生的线程将隐式分离。在这种情况下,派生的线程可能不再连接。注意:程序员有责任最终连接它创建的线程或分离它们,否则将导致资源泄露。

正如用户在spawn的声明中所看到的,对spawn的闭包及其返回值都有两个约束,让我们来解释它们:

(1)静态约束意味着闭包及其返回值必须具有整个程序执行的生存期。这样做的原因是线程可以比创建它们的生存期更长。事实上,如果线程及其返回值可以比它们的调用程序更持久,我们需要确保它们在之后是有效的,因为我们不知道它们什么时候会返回,所以需要让它们尽可能长时间地有效,也就是说,直到程序结束,因此是"静态生存期"。

(2)Send约束是因为闭包需要按值从派生它的线程传递到新线程。它的返回值需要从新线程传递到连接它的线程。作为提醒,Send标记特性表示从一个线程传递到另一个线程是安全的。Sync表示在线程之间传递引用是安全的。

spawn函数的简单示例如下:

use std::thread;



fn main() {

    let handle = thread::spawn(|| {

        //子线程执行的代码

    });

}

子进程也就是主线程的派生线程。其中的||表示闭包,该闭包中的代码将在子线程中执行。调用thread::spawn方法会返回一个句柄,该句柄拥有对线程的所有权。通过这个句柄我们可以管理线程的生命周期和操作线程。thread::spawn 函数接受一个闭包作为参数,闭包中的代码会在子线程中执行。创建的新线程是"分离的",这意味着程序无法了解派生线程何时完成或终止。下面是一个简单的实例。

【例12.1】 创建一个线程

打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。

在main.rs中,添加代码如下:

use std::{ thread, time::Duration };  		//导入线程模块和时间模块

fn main() {
    thread::spawn(|| {   						//创建一个新线程
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

上面代码调用thread::spawn函数创建了一个新的线程,并在该线程中通过一个for循环准备打印9条信息,并且每输出一条信息就调用sleep函数休眠1毫秒。而主线程中的main函数中将打印4条信息,也是每输出一条信息就调用sleep函数休眠1毫秒。我们调用thread::sleep函数强制线程休眠一段时间,这就允许不同的线程交替执行。但要注意的是,当主线程结束的时候,整个进程就结束了,此时派生线程也会结束,所以派生线程中的打印信息是不会全部输出完毕的。也就是说,虽然某个线程休眠时会自动让出CPU,但并不保证其他线程会执行。这取决于操作系统如何调度线程。这个实例的输出结果是随机的,主线程一旦执行完成,程序就会自动退出,不会继续等待子线程。这就是子线程的输出结果为什么不全的原因。

保存文件并运行,运行结果如下:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

可以看到,主线程可以全部输出完毕,而派生线程则没有执行完全部for循环,符合预期。从结果中能看出两件事:第一,两个线程是交替执行的,但是并没有严格的顺序;第二,当主线程结束时,它并没有等子线程运行完。
Thread也支持通过std::thread::Builder结构体进行创建,Builder提供了一些线程的配置项,如线程名字、线程优先级、栈大小等,比如:

use std::thread;

fn main() {
let handle = Builder::new()
    .name("my_thread".to_string()) 		//设置新线程的名称是my_thread
    .stack_size(1024 * 4)   				//设置新线程的堆栈大小是1024*4
    .spawn({
         // 子线程执行的代码
    });
}

12.3.2 等待所有线程完成

在前面的实例中,主线程没等到派生线程执行完毕就结束了,从而整个进程就会结束。那么怎么让派生线程执行完毕呢?答案是通过joinHandle结构体来等待所有线程完成。要了解派生线程何时完成,有必要捕获thread::spawn函数返回的JoinHandle,该结构体声明如下:

pub struct JoinHandle<T>(_);

该结构体通常由thread::spawn函数返回,或者由thread::Builder::spawn函数返回。JoinHandle在关联线程被丢弃时分离该线程,这意味着该线程不再有任何句柄,也无法对其进行连接。由于平台限制,无法克隆此句柄:加入线程的能力是唯一拥有的权限。

该结构体提供了一个函数join,允许调用方(比如主线程)等待派生线程(比如子线程)完成,该函数声明如下:

pub fn join(self) -> Result<T>

该函数等待相关线程完成。如果相关线程已经完成,则函数将立即返回。就原子内存排序而言,相关线程的完成与此函数返回同步。换句话说,该线程执行的所有操作都发生在 join 返回之后发生的所有操作之前。join的返回值通常是子线程执行的结果。

join函数的用法如下:

use std::thread;

let thread_join_handle = thread::spawn(|| {
    //子线程执行的代码
});
//主线程执行的代码
let res = thread_join_handle.join(); 		//等待子线程结束

thread_join_handle存放joinHandle结构,然后调用join方法,可以等待对应的线程执行完成。调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结join方法返回一个线程结果值,如果线程崩溃,则返回错误码,否则返回Ok。

res将得到子线程执行的结果,我们甚至可以在创建线程时,在子线程执行的代码处直接放一个数值或字符串,从而让res得到这个数值或字符串。

如果希望join调用失败时报错一下,可以这样:

thread_join_handle.join().expect("Couldn't join on the associated thread"); //若有问题就会有提示

下面先看一个简单的实例,得到子线程的结果,相当于实现了子线程传递一个值给主线程。

【例12.2】 子线程传递值给主线程

打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:

use std::thread;

fn main() {
    let other_thread = thread::spawn(|| {
         "hello" 		 //这里就写了一个字符串,相当于子线程的执行结果就是字符串"hello"
    });
    let  res = other_thread.join().unwrap();		 //得到子线程执行结果,即"hello"
    println!("{}",res); 
}

保存文件并运行,运行结果如下:

hello

如果有兴趣,还可以把"hello"改为一个整数,那么res就得到这个整数值。我们甚至可以把一个函数返回值作为子线程结果传递给主线程,下面来看一个实例。

【例12.3】 把函数返回值传递给主线程

打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:

use std::thread;
fn thfunc(n: u32) -> u32 { 			//这个函数是线程函数,后面会讲到
    return n+1;
}
fn main() {
    let child = thread::spawn(|| {
        let f = thfunc(30); 			//调用线程函数
        f   							//返回子线程结果,这里也就是函数thfunc的返回值
    });

    let res = child.join().expect("Could not join child thread");
    println!("{}",res);
}

函数thfunc把参数n加1后再返回,并存于f中,然后把f作为子线程的结果,这样主线程通过join函数就可以得到f的值,也就是函数thfunc的返回值。

保存文件并运行,运行结果如下:

31

下面再看一个稍复杂点的实例,加一些循环打印。

【例12.4】 等待子线程执行完毕

打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。

在main.rs中,添加代码如下:

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

fn main() {
    let handle = thread::spawn(|| {  		//返回一个 JoinHandle 类型的值
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap(); 		//阻止当前线程(主线程)执行,并等待子线程执行完毕
}

thread::spawn返回一个JoinHandle类型的值,可以将它存放到变量中。这个类型相当于子线程的句柄,用于连接线程。如果忽略它,就没有办法等待线程。在主线程main函数的结尾,我们调用了join方法来等待子线程执行完毕,即调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结。unwrap 是一个方法,它用于从Option或Result类型中提取值。

保存文件并运行,运行结果如下:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

可以看到,子线程中的for循环全部执行完毕了。

相关推荐
昙鱼5 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
天之涯上上10 分钟前
JAVA开发 在 Spring Boot 中集成 Swagger
java·开发语言·spring boot
2402_8575834911 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
白宇横流学长12 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
kirito学长-Java17 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树18 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring
OTWOL23 分钟前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
问道飞鱼27 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
拓端研究室27 分钟前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin
Code成立28 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing