您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Rust

在 Rust 编程中使用多线程

时间:2024-01-07 14:34:01  来源:微信公众号  作者:二进制空间安全
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。

1. Rust线程实现理念

在大部分现代操作系统中,已执行程序的代码在一个 进程(process)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 线程(threads)。例如,web 服务器可以有多个线程以便可以同时响应多个请求。

将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:

  • 竞态条件(Race conditions),多个线程以不一致的顺序访问数据或资源。
  • 死锁(Deadlocks),两个线程相互等待对方,这会阻止两者继续运行。
  • 只会发生在特定情况且难以稳定重现和修复的 bug。

Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。

编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。

2.使用spawn创建新线程

为了创建一个新线程,需要调用 thread::spawn 函数并传递一个闭包, 并在其中包含希望在新线程运行的代码。看下面的例子:

use std::thread;
use std::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));
    }
}

注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:

thread::sleep 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头,甚至即便我们告诉新建的线程打印直到 i 等于 9,它在主线程结束之前也只打印到了 5。

如果运行代码只看到了主线程的输出,或没有出现重叠打印的现象,尝试增大区间 (变量 i 的范围) 来增加操作系统切换线程的机会。

3.使用join等待所有线程结束

由于主线程结束,上面演示的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,甚至不能实际保证新建线程会被执行!

可以通过将 thread::spawn 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn 的返回值类型是 JoinHandle。JoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。

看下面的示例代码:

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));
    }

    handle.join().unwrap();
}

通过调用 handle 的 join 会阻塞当前线程直到 handle 所代表的线程结束。阻塞(Blocking)线程意味着阻止该线程执行工作或退出。因为我们将 join 调用放在了主线程的 for 循环之后,编译这段代码后运行结果如下:

这两个线程仍然会交替执行,不过主线程会由于 handle.join() 调用会等待直到新建线程执行完毕。

不过如果将 handle.join() 移动到 main 中 for 循环之前会发生什么呢,看下面的代码:

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));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

代码编译执行后结果如下:

主线程会等待直到新建线程执行完毕之后才开始执行 for 循环,所以输出将不会交替出现。

因此,将join放在代码的不同地方, 将会影响线程是否同时执行。

4.将move闭包与线程一起使用

move 关键字经常用于传递给 thread::spawn 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。

为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值, 下面的代码展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作, 代码如下:

use std::thread;

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

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

    handle.join().unwrap();
}

闭包使用了 v,所以闭包会捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在一个新线程中运行这个闭包,所以可以在新线程中访问 v。然而当编译这个例子时,会得到如下错误:

Rust 会 推断 如何捕获 v,因为 println! 只需要 v 的引用,闭包尝试借用 v。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓对 v 的引用是否一直有效。

看一段比较极端情况的代码:

use std::thread;

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

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

    drop(v); // 坏事儿了!

    handle.join().unwrap();
}

如果 Rust 允许这段代码运行,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 v 的引用,不过主线程立刻就使用drop丢弃了v。接着当新建线程开始执行,v 已不再有效,所以其引用也是无效的。

为了修复上面的编译错误, 我们可以根据编译器给予我们的help尝试修正一下,如图:

通过在闭包之前增加 move 关键字,强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。

修正后的代码如下:

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();
}

编译运行试一下:

看起来没问题,那么以这个成功的经验, 修改那段极端情况的代码如下:

use std::thread;

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

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

    drop(v); // 坏事儿了!

    handle.join().unwrap();
}

再次编译一下看看结果如何:

Rust编译器依然没有放行, 这个修复行不通。

如果为闭包增加 move,将会把 v 移动进闭包的环境中, 因此将不能在主线程中对其调用 drop 了, Rust 的所有权规则又一次帮助我们杜绝了隐患。因为 Rust 是保守的并只会为线程借用 v,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 v 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 v。如果对其作出同样的move修改, 那么当在主线程中使用 v 时就会违反所有权规则。move 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。



Tags:Rust 编程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
在 Rust 编程中使用泛型
本文的内容将涉及泛型定义函数、结构体、枚举和方法, 还将讨论泛型如何影响代码性能。1.摘要Rust中的泛型可以让我们为像函数签名或结构体这样的项创建定义, 这样它们就可以...【详细内容】
2024-01-09  Search: Rust 编程  点击:(89)  评论:(0)  加入收藏
在 Rust 编程中使用多线程
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。1. Rust线程...【详细内容】
2024-01-07  Search: Rust 编程  点击:(77)  评论:(0)  加入收藏
征服 Rust 编程世界的终极指南
Rust 适用于构建各种程序,包括 Web 应用、命令行工具和网络服务。Rust 拥有许多现代编程语言应具备的特性,如并发性、类型推断等。自 2015 年发布以来,Rust 已经成为开发者中最...【详细内容】
2023-05-24  Search: Rust 编程  点击:(256)  评论:(0)  加入收藏
软正在用 Rust 编程语言重写核心 Windows 库
微软正在用 Rust 编程语言重写核心 Windows 库。在以色列特拉维夫举办的 BlueHat IL 2023 上,Windows 操作系统安全总监 David “dwizzle” Weston 宣布 Rust 进入操作系统内...【详细内容】
2023-05-04  Search: Rust 编程  点击:(270)  评论:(0)  加入收藏
▌简易百科推荐
在Rust中使用Serde的详细指南
在处理HTTP请求时,我们总是需要在一种数据结构(可以是enum、struct等)和一种可以存储或传输并稍后重建的格式(例如JSON)之间来回转换。Serde是一个库(crate),用于高效、通用地...【详细内容】
2024-03-26  coding到灯火阑珊  微信公众号  Tags:Rust   点击:(13)  评论:(0)  加入收藏
Rust 写脚手架,Clap你应该知道的二三事
有感而发最近,在和前端小伙伴聊天发现,在2024年,她们都有打算入局Rust学习的行列。毕竟前端现在太卷了,框架算是走到「穷途末路」了,无非就是在原有基础上修修补补。所有他们想在...【详细内容】
2024-03-11  前端柒八九  微信公众号  Tags:Rust   点击:(20)  评论:(0)  加入收藏
Rust中的数据可视化指南
可视化是数据分析和解释的一个关键方面。虽然Rust主要以其性能和安全特性而闻名,但它也为数据可视化提供了强大的工具。在这个全面的指南中,我们将深入研究Rust中的数据可视化...【详细内容】
2024-03-07  coding到灯火阑珊  微信公众号  Tags:Rust   点击:(29)  评论:(0)  加入收藏
如何在Rust中操作JSON,你学会了吗?
sonic-rs ​还具有一些额外的方法来进行惰性评估和提高速度。例如,如果我们想要一个 JSON​ 字符串文字,我们可以在反序列化时使用 LazyValue​ 类型将其转换为一个仍然带有斜...【详细内容】
2024-02-27  前端柒八九  微信公众号  Tags:Rust   点击:(47)  评论:(0)  加入收藏
记一次Rust内存泄漏排查之旅
在某次持续压测过程中,我们发现 GreptimeDB 的 Frontend 节点内存即使在请求量平稳的阶段也在持续上涨,直至被 OOM kill。我们判断 Frontend 应该是有内存泄漏了,于是开启了排...【详细内容】
2024-02-27  OSC开源社区    Tags:Rust   点击:(12)  评论:(0)  加入收藏
Rust 最受欢迎的这些库
今天分享主题是,关于一些值得注意的 Rust 库,这些库可以根据它们的功能和在编码中的受欢迎程度进行选择。什么是 Rust 库?在 Rust 中,常被称为 “crate” 的库,是一个打包的单元...【详细内容】
2024-02-19  码农渔夫  微信公众号  Tags:Rust   点击:(50)  评论:(0)  加入收藏
异步Rust:构建实时消息代理服务器
在本文中,我们将深入研究使用Rust构建实时消息代理服务器,展示其强大的并发特性。我们将使用Warp作为web服务器,并使用Tokio来管理异步任务。此外,我们将创建一个WebSocket客户...【详细内容】
2024-02-01      Tags:Rust   点击:(57)  评论:(0)  加入收藏
在 Rust 编程中使用泛型
本文的内容将涉及泛型定义函数、结构体、枚举和方法, 还将讨论泛型如何影响代码性能。1.摘要Rust中的泛型可以让我们为像函数签名或结构体这样的项创建定义, 这样它们就可以...【详细内容】
2024-01-09  二进制空间安全  微信公众号  Tags:Rust   点击:(89)  评论:(0)  加入收藏
什么是Rust语言 ,特点是什么,跟其它语言对比有什么优势
什么是RustRust是一种系统编程语言,旨在提供高性能和安全性。它是由Mozilla和其开发社区创建的开源语言,设计目标是在C++的应用场景中提供一种现代、可靠和高效的选择。Rust的...【详细内容】
2024-01-09    简易百科  Tags:Rust语言   点击:(203)  评论:(0)  加入收藏
在 Rust 编程中使用多线程
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。1. Rust线程...【详细内容】
2024-01-07  二进制空间安全  微信公众号  Tags:Rust 编程   点击:(77)  评论:(0)  加入收藏
站内最新
站内热门
站内头条