《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
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循环全部执行完毕了。