Rust线程模型与线程创建

【赠书活动第4期】《Rust编程与项目实战》-CSDN博客

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

Rust线程模型

一个正在执行的Rust程序由一组本地操作系统线程组成,每个线程都有自己的堆栈和本地状态。线程可以被命名,并为低级别同步提供一些内置支持。

线程之间的通信可以通过通道、Rust的消息传递类型以及其他形式的线程同步和共享内存数据结构来完成。特别是,保证线程安全的类型可以使用原子引用计数容器Arc在线程之间轻松共享。

Rust中的致命逻辑错误会导致线程死机(也称崩溃),在此期间,线程将展开堆栈,运行析构函数并释放所拥有的资源。Rust中的线程死机可以用catch_unwnd捕获(除非使用panic=abort编译)并从中恢复,或者用resume_unwnd恢复。如果没有捕捉到死机,线程将退出,但可以选择从具有连接的其他线程检测到死机。如果主线程死机而没有捕获到死机,则应用程序将使用非零的退出代码退出。

当Rust程序的主线程终止时,即使其他线程仍在运行,整个程序也会关闭。然而,该模块为自动等待线程的终止(即加入)提供了方便的设施。

Rust 中的多线程通过 std::thread模块来实现,它提供了创建和管理线程的功能。Rust 的多线程模型采用"共享状态,可变状态"(Shared State,Mutable State)的方式,这意味着多个线程可以访问同一个数据,但需要通过锁(Lock)来保证数据的安全性。

注意,要使用Rust标准库中的线程函数,通常要在文件开头包含std::thread,比如:

use std::thread;

模块std::thread

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】 创建一个线程

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

(2) 在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,但并不保证其他线程会执行。这取决于操作系统如何调度线程。这个实例的输出结果是随机的,主线程一旦执行完成,程序就会自动退出,不会继续等待子线程。这就是子线程的输出结果为什么不全的原因。

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

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({
         // 子线程执行的代码
    });
}
相关推荐
程序伍六七1 分钟前
day16
开发语言·c++
wkj0016 分钟前
php操作redis
开发语言·redis·php
极客代码11 分钟前
【Python TensorFlow】进阶指南(续篇三)
开发语言·人工智能·python·深度学习·tensorflow
土豆湿16 分钟前
拥抱极简主义前端开发:NoCss.js 引领无 CSS 编程潮流
开发语言·javascript·css
界面开发小八哥23 分钟前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具
草莓base37 分钟前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...1 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
qystca1 小时前
洛谷 B3637 最长上升子序列 C语言 记忆化搜索->‘正序‘dp
c语言·开发语言·算法
编程重生之路1 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
薯条不要番茄酱1 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea