在Rust中,每个进程最初只有一条主执行路径,被称为主线程。main函数执行的就是主线程。我们可以创建额外的线程,以实现并行执行任务或操作。Rust中的线程本质上就是操作系统线程或物理线程,Rust中的线程与操作系统线程存在一一对应的关系,这与一些语言的绿色线程(M: N线程模型)不同,绿色线程将多个逻辑线程(M)调度到少数个物理线程(N)上运行。
并行和并发变成是引入多线程模型的两个重要原因。
- 并行编程旨在将一个进程拆分为多个并行操作,以提升整体性能。
- 并发致力于提供系统的响应能力。例如在执行排序等计算密集型操作时,仍然能保持用户界面的响应。
你可能会觉得既然2个线程可以提升性能,那10个线程效果岂不是更好?然而事实并非如此。
决定是否带来性能改善的因素有很多,比如与操作系统相关的因素。一旦线程数量超过某个临界点,反而会导致性能下降。因为存在数据依赖和线程运行时开销(如上下文切换),所以无法做到完全并行。
在Rust中,无畏并发的设计消除了并发变成的多种顾虑(数据竞争等)。
在一个进程这个空间中,每个线程也拥有私有资源。其中值得关注的是线程栈,用于维护该线程的局部变量、系统调用信息以及其他信息。在某些环境下,线程的默认栈大小可能是2MiB,对于拥有数十甚至上百个线程而言,栈空间的总占用是一笔相当大的内存开销。Rust为开发者提供了管理线程栈大小的机制,可以帮助你更好地管理内存占用。
相比于单线程,多线程的管理更加复杂
- 竞态条件,多个线程竞争贡献资源的情况。
- 死锁,一个线程无限期地等待另一个线程或资源变得可用。
- 不一致性,多线程应用程序如果实现不当会表现出不一致性。
尽管增加了复杂性,多线程仍然是创建可扩展、响应快、高性能应用程序的一个重要工具。
同步函数调用
fn hello() {
println!("hello, world");
}
fn main() {
println!("In main");
hello();
println!("Back in main");
}
每个函数都有局部变量,这些变量被放置在保存线程状态的栈上。
fn display() {
let b = 2;
let c = 3.4;
println!("{} {}", b, c);
}
fn main() {
let a = 1;
println!("{}", a);
display();
}
每个函数都会获得一块专门的存储区域,被称为栈帧,用于保存自己的私有数据。随着同步函数调用的不断深入,新的栈帧被持续压入栈中,导致栈的空间持续增长。当函数执行完毕后,对应的栈帧会从栈中移除。
线程
Rust标准库中的thread模块提供了线程相关的功能。
- thread::spawn 该函数只接收一个参数(函数/闭包)作为新线程的入口点,用于创建并立即启动一个新线程。
pub fn spawn<F, T>(f: F)->JoinHandle<T>
where F: FnOnce()->T+Send+'static
T: Send+'static
需要注意的是
- spawn返回一个JoinHandle,用于线程同步和获取线程入口函数的返回值。
- F是入口函数的类型参数,T是线程返回值的类型参数。
- Send约束了该值可以安全地跨线程传递。
- 'static生命周期是必须的,因为我们无法预知线程何时启动和结束,新线程的生命周期可能超过父线程,因此T和F都要求具有静态生命周期。
- 父线程只是一个比喻性的说法,实际上这两个线程之间没有关系。
多线程示例
use std::thread;
fn main() {
let thread = thread::spawn(|| println!("hello"));
println!("In main");
}
main函数和闭包在独立的线程中同时运行。
这个例子会出现不稳定行为:
- 如果main函数率先完成,则程序退出,包括终止其他正在运行的线程。闭包的问候信息来不及显示
- 这两个线程的执行顺序是不确定的,当你多次运行代码,结果可能会不同。问候信息可能显示也可能不显示。
spawn函数会返回一个JoinHandle,我们可以用其join方法让当前线程等待,直到由它管理的线程执行完毕后才继续执行。这种等待一直持续到关联的线程被分离,例如被丢弃。
use std::thread;
fn main() {
let thread = thread::spawn(||println!("hello"));
println!("In main");
let ret = thread.join(); // 等待线程结束
println!("Break in main");
}
有时候可能需要获取线程的执行结果,也可以用JoinHandle,其join方法会阻塞当前线程,直到与其关联的线程执行完毕,join方法的返回值就是该线程的返回值(Ok(value))。
use std::thread;
fn main() {
let t = thread::spawn(|| 1);
let ret = t.join();
println!("{}", ret.unwrap());
}
如果一个正在运行的线程没有成功完成执行,例如遇到了panic
- 对于主线程(通常是main函数),那么整个程序将终止。
- 对于非主线程,线程将在栈展开后简单地终止,但其他线程将会继续执行。
如果该线程在join列表中,那么join函数会返回Err结果,作为对发生panic的通知。
use std::thread;
fn main() {
let handle = thread::spawn(|| panic!("kaboom"));
let ret = handle.join();
match ret {
Ok(value) => println!("{}",value),
Err(msg) => println!("{:?}", msg),
}
}
当线程由闭包创建时,可以通过捕获变量将数据传递给线程。这是线程常见的输入来源。然而线程异步方式运行,不受父线程作用域的限制。新线程的存在时间可能比父线程更长。因此为了避免数据所有权的问题,需要使用move关键字将捕获的数据所有权转移到闭包中。
use std::thread;
fn main() {
let a = 10;
let b = 20;
let handle = thread::spawn(move || {
let c = a + b;
println!("result: {}", c)
});
let result = handle.join();
println!("{:?}", result.unwrap());
}
作用域线程消除了普通线程使用捕获变量的一些限制。更重要的,作用域线程的生命周期是确定的。它不会比创建它的代码块(作用域)存活更久,因此不需要move关键字。
创建作用域线程需要使用thread::scope函数。该函数接收一个作用域对象作为参数,该对象定义了作用域线程的生存范围(作用域)。
fn scope<'env, F, T>(f: F)->T
where F: for<'scope> FnOnce(&'scope &Scope<'scope, 'env>)->T,
类型参数F将作用域对象描述为函数,

最低0.47元/天 解锁文章
984

被折叠的 条评论
为什么被折叠?



