Rust多线程中安全的使用变量

news/2025/1/26 14:22:44/文章来源:https://www.cnblogs.com/wang_yb/p/18691766

在Rust语言中,一个既引人入胜又可能带来挑战的特性是闭包如何从其所在环境中捕获变量,尤其是在涉及多线程编程的情境下。

如果尝试在不使用move关键字的情况下创建新线程并传递数据至闭包内,编译器将很可能返回一系列与生命周期借用规则所有权相关的复杂错误信息。

不过,这种机制虽然增加了学习曲线,但也确保了内存安全与并发执行中的数据一致性。

本文我们将探讨如何在线程的闭包中安全的使用变量,包括共享变量和修改变量。

1. 向线程传递变量

首先,我们构造一个简单的示例,在线程中正常使用一个外部的变量,看看Rust中能否正常编译运行。

use std::thread;fn main() {let msg = String::from("Hello World!");let handle = thread::spawn(|| {// msg 是主线中定义的变量println!("{}", msg);});handle.join().unwrap();
}

例子非常简单,看着写法也没什么问题,在其他编程语言中类似的写法是没有问题的。

但是,使用cargo run运行时,却有如下的错误:

为什么会有这样的错误?这就是Rust在内存方面更加严谨的原因。

上面Rust的错误信息中也给出了原因,总结起来主要有两点:

  1. 线程的生命周期:新创建的线程的生命周期有可能超出主函数 main 的执行范围。当 main 函数终止时,与之相关的局部变量(也就是msg)将超出作用域。
  2. 不符合借用规则:在 Rust 中,引用的生命周期不会超过其所指向数据的生命周期,以避免出现悬空引用。如果main提前结束,那么线程中的msg将成为悬空引用

修复的方法很简单,使用move关键字,将变量的所有权转移到线程中就可以了。

    let handle = thread::spawn(move || {// msg 是主线中定义的变量println!("{}", msg);});

这样就可以正常运行了。

不过,这样,主线程中就无法使用变量msg了,比如在main函数的最后打印msg,会报错,因为它的所有权已经转移到线程中了。

2. 多线程共享变量引用

如果我们只把变量的引用转移给线程,是不是可以在主线程main中继续使用变量msg呢?

use std::thread;fn main() {let msg = String::from("Hello World!");let msg_ref = &msg;let handle = {thread::spawn(move || {// msg 是主线中定义的变量println!("{}", msg_ref);})};handle.join().unwrap();println!("msg in main : {}", msg_ref);
}

很遗憾,依然有错误:

错误的原因仍然是传入线程中的变量引用msg_ref生命周期的不够长。

虽然我们使用了move,将msg_ref转移到线程中,但main中仍然拥有底层的数据msg

一旦main函数结束(或者数据在线程完成之前超出范围),该引用(msg_ref)指向数据将失去有效的内存,成为悬空引用

总的来说就是:

  1. 移动引用并不移动原始数据-只转移引用本身的所有权
  2. 实际数据(msg)仍然由原始范围拥有,并具有自己的生命周期约束

为了修复这个错误,就要用到Rust中提供的并发原语Arc(一种自动引用计数的智能指针)。

先看看使用Arc修改后的例子。

use std::sync::Arc;
use std::thread;fn main() {let msg = String::from("Hello World!");// 通过Arc来创建变量的引用let msg_ref = Arc::new(msg);// 线程1let handle_1 = {// move 之前,先使用Arc clone 变量let msg_thread = Arc::clone(&msg_ref);thread::spawn(move || {println!("Thread 1: {}", msg_thread);})};// 线程2let handle_2 = {let msg_thread = Arc::clone(&msg_ref);thread::spawn(move || {println!("Thread 2: {}", msg_thread);}) };handle_1.join().unwrap();handle_2.join().unwrap();// 主线程中依然可以使用变量println!("msg in main : {}", msg_ref);
}

使用Arc修改之后,变量不仅可以在多个线程中共享,主线程中也可以使用。

3. 多线程中修改变量

上面的示例是在多个线程中共享变量,如果想要修改变量的话,那么就会出现数据竞争的情况。

这时,就要用到Rust的另一个并发原语Mutex

use std::sync::{Arc, Mutex};
use std::thread;fn main() {// 创建一个被Mutex保护的共享数据,这里是一个i32类型的数字let shared_number = Arc::new(Mutex::new(0));// 定义一个线程向量,用于存储创建的线程let mut threads = Vec::new();// 创建10个线程,每个线程对共享数据进行1000次递增操作for _ in 0..10 {// 克隆Arc,使得每个线程都拥有一个指向共享数据的引用let num_clone = Arc::clone(&shared_number);let handle = thread::spawn(move || {// 尝试获取Mutex的锁,这是一个阻塞操作,如果锁不可用,线程会等待let mut num = num_clone.lock().unwrap();for _ in 0..1000 {*num += 1;}});threads.push(handle);}// 等待所有线程完成操作for handle in threads {handle.join().unwrap();}// 获取最终的共享数据值并打印let final_num = shared_number.lock().unwrap();println!("最终10个线程的累加结果: {}", final_num);
}

在这个示例中:

  1. 首先创建了一个Arc<Mutex<i32>>类型的共享数据,Arc用于在多个线程间共享MutexMutex用于保护内部的i32数据。
  2. 循环创建10个线程,每个线程都克隆了Arc并尝试获取Mutex的锁。一旦获取到锁,线程就可以安全地对共享数据进行递增操作。
  3. 主线程使用join方法等待所有子线程完成操作。
  4. 最后,主线程获取并打印共享数据的最终值。由于Mutex的保护,多个线程对共享数据的操作不会产生数据竞争,保证了数据的一致性。

运行结果:

10个线程,每个累加1000,所以最后结果是1000*10=10000

4. 总结

从上面的例子可以看出,Rust的闭包捕获规则最初可能感觉很严格,但它们在确保内存安全数据竞争自由方面至关重要。

总之,

如果需要在另一个线程中拥有数据,考虑使用move

如果需要跨线程共享数据,考虑使用Arc

如果需要跨线程共享和修改数据,考虑使用Arc+Mutex

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/875962.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ARC_069 D - Menagerie 题解

atcoder 一道很有意思的模拟题啊。 思路很重要。 首先,我们只要知道连续两只动物的身份,就可以根据 \(s\) 推出所有动物的身份。 不妨假设我们知道第一只和第二只动物的身份,一共有几种情况呢? 用 \(1\) 代表羊,\(0\) 代表狼。 那么,共有 \(2^2=4\) 种情况,分别为: 00 …

『学习笔记』二分算法

今天记录二分知识点。 二分是一个简单清晰,实用性强的算法。 也是本人最喜欢的算法之一。 先给出二分模板吧!int l = 1, r = n;//初始值,根据情况而定while (l + 1 < r) {int mid = (l + r) >> 1;if (check(mid)) l = mid;// check函数判断左半部分是否不符合,更新…

回家之难难于蜀道难

回家难 之难于蜀道难 (仿写李白蜀道难)噫吁嚱,困乎难乎,回家之途,难于上班路。 盘古及女娲,开天辟地捏人烟,尓来文明已万年,难解归家争吵事。 游子无钱难上路,漂留外地护空城。 千思万想定下来,踏上归途望团年。 上有爸妈在老家,下有孩童八九岁。 列车无票不得行,驱…

MAC|Edge——下载视频

解码错误解码错误指的是当前音/视频帧与浏览器不兼容,可以尝试以下方式:1.chrome/edge 浏览器打开chrome://flags,搜索 Hardware-accelerated video decode,选择 disabled2.如果解码错误仍然存在,请对视频进行转码处理,以修复问题帧3.firefox浏览器请打开about:support,…

stdio.h的缓冲机制解析

在C语言中,由于stdio.h中的缓冲机制,printf的输出常令人感到迷惑。本文将介绍其缓冲机制的具体细节1. 令人迷惑的printf() 在C语言中,由于stdio.h中的缓冲机制,printf的输出通常会受到缓冲区的影响。 这种影响可能非常微妙,并常常令人疑惑,比如我们来看下面这段代码 #inc…

【新能源行业】新能源汽车电子驻车制动系统(EPB)谁在做?

长期以来,汽车的动力系统一直是人们所关注的焦点,然而,汽车制动系统在背后默默支撑起整个汽车安全与稳定。其重要性丝毫不亚于动力系统。行车上路,安全第一。在每一次的启程与停驻之间,唯有制动系统作为坚实保障,才能让每一次出行都安心无虞。一、制动系统分类与组成 目前…

如何从内存中提取shellcode

恶意程序有时会直接在内存中运行shellcode 。在这篇文章中,我将向你展示如何从内存中获取shellcode。 shellcode在内存中的位置 在内存中分配shellcode的常用方法是使用VirtualAlloc来分配具有所需权~限的内存。然后恶意软件使用RtlMoveMemory将shellcode写入分配的空间。然后…

施耐德UNITY中使用ST 语言计算日均值

以前做过练习,在unity中计算分钟均值和小时均值,做成自定义功能块。今天在家打算按照同样的思路,试着做一下日均值。 第一次打算建立一个三维数组PV_DAY[0..23,0..59,0..59],每秒存放一个数据,编译的时候提示数组太大。 第二次尝试建立24个数组,每个数组存放一个小时内36…

【转载】rpm 和 yum 软件包的应用

本节所讲内容:8.1 使用rpm命令-安装-查看-卸载-rpm软件包8.2 yum管理软件包8.3 CentOS8中使用DNF管理软件包8.4 实战tar源码包管理-源码包安装方法8.1 软件包的管理软件包的类型rpm二进制包------》已经使用GCC编译后的(二进制已经可以被操作系统直接执行了)tar源码包-----》…

[Redis] Redis (5) 多核多线程架构

序 引言Redis 作为一款高性能的内存数据库,以其简单的设计和单线程模型(潜台词:单核单线程)广受欢迎。 然而,随着用户需求和数据规模的增长,单线程的架构逐渐成为 Redis 性能的瓶颈。 近年来,Redis 开始引入部分多线程机制,以提高并发性能,特别是在处理网络 I/O 和数据持…

Python并行计算与高性能计算7迎接并行计算革命

在本章中,我们将介绍我们在前几章中看到的并行编程的实际方面。随着并行计算概念的扩展,它不仅包括并行编程及其相关方面,还包括能够管理并专门设计的基础设施。超级计算机通常被定义为由许多 CPU 和 GPU 组成的高性能系统,其中应用了并行计算和高性能计算 (HPC) 方法。本章…

人脸识别和神经风格转换

人脸识别和神经风格转换 人脸识别人脸验证(Verification):验证输入图像是否属于某个特定身份,属于一对一问题。 人脸识别(Recognition):一对多问题,从大量数据中找到匹配的人脸。 在很多人脸识别应用中,系统需要通过单一样本识别某人,而非多个样本,这就属于 One-shot Le…