Rust如何使用线程同时运行代码

 更新时间:2025年02月26日 08:55:02   作者:Hello.Reader  
Rust使用1:1线程模型,通过std::thread::spawn创建线程,返回JoinHandle用于等待线程完成,闭包默认借用外部变量,使用move关键字转移所有权,多线程共享数据时需使用并发原语,如Mutex、RwLock、Arc等,以避免竞态条件

一、Rust 的线程模型

Rust 标准库使用的是 1:1 的线程模型,即每一个语言层的线程都对应一个操作系统线程。Rust 中通过标准库提供的 std::thread 模块来创建、管理线程。

当然,也有一些第三方库会采用不同的线程模型,或者利用异步(async)机制来实现并发(比如 Rust 的 async/await 机制),在面对具体需求时可以根据实际情况做选择。

二、创建线程:thread::spawn

要在 Rust 中创建一个新线程,可以使用 thread::spawn 函数,并向它传递一个闭包(closure)。闭包中包含需要在线程中执行的代码。

例如:

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

fn main() {
    // 使用 thread::spawn 创建新的线程
    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));
    }
}

在上面的例子里,我们在子线程中打印数字,同时在主线程中也打印数字。由于操作系统会对线程进行调度,输出的顺序无法完全预测。可能主线程先打印,也可能子线程先打印,或者两者交错执行。

需要注意的是,当主线程结束时,所有通过 spawn 创建的子线程会被强制终止,即使子线程还没有执行完。

三、等待线程完成:JoinHandle 与 join

如果希望确保子线程的代码一定会执行完,那么就需要在主线程结束前等待子线程。thread::spawn 的返回值是一个 JoinHandle,可以用它来调用 join 方法,阻塞(block)当前线程,直到对应的子线程执行完成。

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

fn main() {
    let handle = 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));
    }

    // 调用 join,阻塞主线程,直到子线程结束
    handle.join().unwrap();
}

当我们在主线程中调用 handle.join() 时,主线程会暂停执行,直到子线程完成工作。这样就能确保在程序退出前,所有线程都能顺利完成执行。

如果把 join 放在主线程的循环之前,那么主线程会先等待子线程结束,才会进行自身的打印操作——这样就不会再看到主线程与子线程的输出交错了。

四、move 闭包与线程

多线程编程中常常需要在线程间传递数据或访问主线程中的变量。

在 Rust 中,如果一个闭包想要捕获外部变量,就要考虑该变量的所有权或引用生命周期问题。

4.1.问题场景

如下示例所示,如果我们在主线程中创建一个向量 v,然后在子线程中直接打印这个向量,就会出错:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    // ...
    handle.join().unwrap();
}

编译时,Rust 会提示闭包捕获的是对 v 的引用,但无法保证在子线程运行时 v 依旧有效:主线程可能在子线程使用 v 之前就结束了,让 v 不再有效,从而导致潜在的悬垂引用(dangling reference)。

4.2.使用 move 关键字

为了解决这个问题,需要在闭包前面加上 move 关键字,这样可以把闭包中用到的外部数据移动到闭包的所有权中。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

在这里,move 会把 v 的所有权从主线程转移到子线程,从而保证子线程在使用 v 时不会遇到生命周期问题。不过需要注意的是,这样一来,主线程就不能再使用 v 了,因为所有权已经被移动出去了。

4.3.不能与 drop 共用所有权

如果尝试同时在主线程中显式调用 drop(v) 并且在子线程中使用 v,无论有没有用 move,都行不通。Rust 的所有权规则会保证同一份数据不会被多次释放或引用到失效的数据。所以,在设计多线程逻辑时,需要明确划分数据的所有权与生命周期,以避免死锁、竞态条件或悬垂引用等问题。

五、小结

  1. Rust 标准库中的线程模型:Rust 使用一对一(1:1)模型,每个语言线程对应一个系统线程。
  2. 创建线程:使用 thread::spawn 来创建子线程,传入一个闭包作为要执行的代码。
  3. 线程同步:通过返回的 JoinHandle 调用 join,可以阻塞主线程并等待子线程完成执行。
  4. 所有权与生命周期:使用 move 关键字将闭包所需的变量从主线程移动到子线程,从而避免引用冲突或无效引用。
  5. 小心共享数据:当多个线程需要同时访问或修改同一份数据时,需要使用安全的并发原语(例如 MutexRwLockArc 等),否则会出现竞态条件。

在 Rust 中编写并发程序时,我们需要充分利用所有权与借用检查器提供的安全保障,同时对多线程逻辑进行精心设计。尽管多线程编程能带来性能上的提升,但也应关注潜在的风险,并通过 Rust 的工具链和语言特性来尽量减少错误,写出更安全、更可靠的并发应用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Rust使用Sled添加高性能嵌入式数据库

    Rust使用Sled添加高性能嵌入式数据库

    这篇文章主要为大家详细介绍了如何在Rust项目中使用Sled库,一个为Rust生态设计的现代、高性能嵌入式数据库,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-03-03
  • Rust中的&和ref使用解读

    Rust中的&和ref使用解读

    在Rust中,`&`和`ref`都可以用来定义指针,但它们的使用位置不同,`&`通常放在等号右边,而`ref`放在左边,`&`主要用于函数参数和模式匹配中,而`ref`主要用于模式匹配中,Rust通过`&`和`ref`提供了灵活的指针操作,使得代码更加安全和高效
    2025-02-02
  • rust将bitmap位图文件另存为png格式的方法

    rust将bitmap位图文件另存为png格式的方法

    通过添加依赖,转换函数和单元测试操作步骤来解决将bitmap位图文件另存为png格式文件,本文通过实例代码给大家介绍的非常详细,对rust bitmap位另存为png格式的操作方法感兴趣的朋友一起看看吧
    2024-03-03
  • Rust生命周期常见误区(中英对照)全面指南

    Rust生命周期常见误区(中英对照)全面指南

    这篇文章主要WEIDJAI 介绍了Rust生命周期常见误区(中英对照)的全面指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • 解读Rust的Rc<T>:实现多所有权的智能指针方式

    解读Rust的Rc<T>:实现多所有权的智能指针方式

    Rc<T> 是 Rust 中用于多所有权的引用计数类型,通过增加引用计数来管理共享数据,只有当最后一个引用离开作用域时,数据才会被释放,Rc<T> 适用于单线程环境,并且只允许不可变共享数据;需要可变共享时应考虑使用 RefCell<T> 或其他解决方案
    2025-02-02
  • rust多样化错误处理(从零学习)

    rust多样化错误处理(从零学习)

    一个优秀的项目,错误处理的优雅性是至关重要的,而rust,anyhow creat是绕不过去的一个,今天我们来研究下,怎么使用它,帮助我们写出更优雅的代码,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2023-11-11
  • 探索Rust切片与Go有何区别

    探索Rust切片与Go有何区别

    这篇文章主要为大家介绍了Rust切片与Go的区别探索,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • 一文带你了解Rust是如何处理错误的

    一文带你了解Rust是如何处理错误的

    程序在运行的过程中,总是会不可避免地产生错误,而如何优雅地解决错误,也是语言的设计哲学之一。本文就来和大家来了Rust是如何处理错误的,感兴趣的可以了解一下
    2022-11-11
  • Rust HashMap详解及单词统计示例用法详解

    Rust HashMap详解及单词统计示例用法详解

    HashMap在Rust中是一个强大的工具,通过合理使用可以简化很多与键值对相关的问题,在实际开发中,我们可以充分利用其特性,提高代码的效率和可读性,本文将深入介绍HashMap的特性,以及通过一个单词统计的例子展示其用法,感兴趣的朋友一起看看吧
    2024-02-02
  • Rust的泛型、Traits与生命周期用法及说明

    Rust的泛型、Traits与生命周期用法及说明

    本文通过一个寻找列表中最大值的示例,展示了如何从重复代码中提取函数,再利用泛型实现代码复用,主要步骤包括:识别重复逻辑;抽象提取;泛型应用;进一步扩展,通过不断抽象和泛化,我们不仅能减少代码重复,还能写出更通用、健壮和可维护的代码
    2025-02-02

最新评论