Rust多线程详解

在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

需要注意的是

  1. spawn返回一个JoinHandle,用于线程同步和获取线程入口函数的返回值。
  2. F是入口函数的类型参数,T是线程返回值的类型参数。
  3. Send约束了该值可以安全地跨线程传递。
  4. 'static生命周期是必须的,因为我们无法预知线程何时启动和结束,新线程的生命周期可能超过父线程,因此T和F都要求具有静态生命周期。
  5. 父线程只是一个比喻性的说法,实际上这两个线程之间没有关系。

多线程示例

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将作用域对象描述为函数,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值