【Rust自学】16.3. 共享状态的并发
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
16.3.1. 使用共享来实现并发
还记得Go语言有一句名言是这么说的:Do not communicate by sharing memory; instead, share memory by communicating.(不要用共享内存来通信,要用通信来共享内存)
上一篇文章就是使用通信的方式来实现并发的。这一篇文章讲一下如何使用共享内存的方式来实现并发。Go语言不建议使用这种方式,Rust支持通过共享状态来实现并发。
上一篇文章讲的Channel
类似单所有权:一旦值的所有权转移至Channel
,就无法使用它了。共享内存并发类似于多所有权:多个线程可以同时访问同一块内存。
16.3.2. 使用Mutex
来只允许一个线程来访问数据
Mutex
是mutual exclusion(互斥锁)的简写。
在同一时刻,Mutex
只允许一个线程来访问某些数据。
想要访问数据,线程必须首先获取互斥锁(lock),在Rust里就是调用lock
方法获得。lock
数据结构是Mutex
的一部分,它能跟踪谁对数据拥有独占访问权。Mutex
通常被描述为:通过锁定系统来保护它所持有的数据。
16.3.3. Mutex
的两条规则
- 在使用数据之前,必须尝试获取锁(lock)。
- 使用完
Mutex
所保护的数据,必须对数据进行解锁,以便其他线程可以获取锁。
16.3.4. Mutex<T>
的API
通过Mutex::new
函数来创建Mutex<T>
,其参数就是要保护的数据。Mutex<T>
实际上是一个智能指针。
在访问数据前,通过lock
方法来获取锁,这个方法会阻塞当前线程的运行。lock
方法也可能会失败,所以返回的值被Result
包裹,如果成功其值,Ok
变体附带的值的类型就为MutexGuard
(智能指针,实现了Deref
和Drop
)。
看个例子:
use std::sync::Mutex;fn main() {let m = Mutex::new(5);{let mut num = m.lock().unwrap();*num = 6;}println!("m = {m:?}");
}
- 使用
Mutex::new
创建了一个互斥锁,其保护的数据是5,赋给m
。所以m
的类型是MutexGuard<i32>
。 - 后面使用
{}
创建了新的小作用域,在小作用域里使用lock
方法获取值,使用unwrap
进行错误处理。由于MutexGuard
实现了Deref
trait,我们就可以获得内部数据的引用。所以num
是一个可变引用。 - 在小作用域内还使用了解引用
*
来修改数据的值为6。 - 由于
MutexGuard
实现了Drop
trait,所以在小作用域结束后会自动解锁。 - 最后打印了修改后的互斥锁内的内容。
输出:
m = Mutex { data: 6 }
16.3.5. 多线程共享Mutex<T>
看个例子:
use std::sync::Mutex;
use std::thread;fn main() {let counter = Mutex::new(0);let mut handles = vec![];for _ in 0..10 {let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
counter
实际上就是一个计数器,只是使用了Mutex
包裹以更好地在多线程中调用,刚开始的值是0handle
目前是一个空Vector
- 下面通过从0到10(不包括10)的循环创建了10个线程,把每个线程得到的
handle
放到空集合handles
里。 - 在线程的闭包里,我们的意图是把
counter
这个互斥锁转移到闭包里(所以使用了move
关键字),然后获取互斥锁,然后修改它的值,每个线程都加1。当线程执行完后,num
会离开作用域,互斥锁被释放,其他线程就可以使用了。 - 从0到10(不包括10)的循环里还遍历了
handles
,使用join
方法,这样等每个handle
所对应的线程都结束后才会继续执行。 - 最后在主线程里尝试获得
counter
的互斥锁,然后把它打印出来。
输出:
$ cargo runCompiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`--> src/main.rs:21:29|
5 | let counter = Mutex::new(0);| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {| -------------- inside of this loop
9 | let handle = thread::spawn(move || {| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());| ^^^^^^^ value borrowed here after move|
help: consider moving the expression out of the loop so it is only moved once|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();|For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
错误是在前一次循环中已经把所有权移到前一次的那个线程里了,而这一次循环就没发再获得所有权了。
那么如何把counter
放到多个线程,也就是让多个线程拥有它的所有权呢?
16.3.6. 多线程的多重所有权
在15章讲了一个多重所有权的智能指针叫Rc<T>
,把counter
用Rc
包裹即可:
let counter = Rc::new(Mutex::new(0));
在循环里,需要把克隆传进线程,这里用了类型遮蔽把新counter
值设为旧counter
的引用:
let counter = Rc::clone(&counter);
修改后的代码(记得在使用前引入Rc
):
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;fn main() {let counter = Rc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter = Rc::clone(&counter);let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
输出:
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely--> src/main.rs:11:36|
11 | let handle = thread::spawn(move || {| ------------- ^------| | || ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`| | || | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely|= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send`
note: required because it's used within this closure--> src/main.rs:11:36|
11 | let handle = thread::spawn(move || {| ^^^^^^^
note: required by a bound in `spawn`--> /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/std/src/thread/mod.rs:688:1For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
看报错信息的这部分:`Rc<Mutex<i32>>` cannot be sent between threads safely
,Rc<Mutex<i32>>
不能在线程间安全地传递。编译器也告诉我们了原因:the trait `Send` is not implemented for `Rc<Mutex<i32>>`
,Rc<Mutex<i32>>
没有实现send
trait(下一篇文章会讲到)。只有实现send
的类型才能在线程间安全地传递。
其实在第15章讲Rc<T>
也说到了它不能用于多线程场景:Rc<T>
不能安全地跨线程共享。它不能确保计数的更改不会被另一个线程中断。这可能会导致错误的计数,进而导致内存泄漏或在我们完成之前删除某个值。我们需要的是一种与Rc<T>
完全相同的类型,但它以线程安全的方式更改引用计数。
那么多线程应该用什么呢?有一个智能指针叫做Arc<T>
可以胜任这个场景。
16.3.7. 使用Arc<T>
来进行原子引用计数
Arc<T>
和Rc<T>
类似,但是它可以用于并发场景。Arc
的A指的是Atomic(原子的),这意味着它是一个原子引用计数类型,原子是另一种并发原语。这里不对Arc<T>
做过于详细的介绍,只需要知道原子像原始类型一样工作,但可以安全地跨线程共享,其余信息详见Rust官方文档。
那么为什么所有的基础类型都不是原子的?为什么标准库不默认使用Arc<T>
?这是因为:
Arc<T>
的功能需要以性能作为代价Arc<T>
和Rc<T>
的API都是相同的
既然Arc<T>
和Rc<T>
的API都是相同的,那么先前的代码就很好改了(记得在使用前引入Arc
):
use std::sync::{Arc, Mutex};
use std::thread;fn main() {let counter = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter = Arc::clone(&counter);let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
16.3.8. RefCell<T>
/Rc<T>
vs. Mutex<T>
/Arc<T>
Mutex<T>
提供了内部可变性,和Cell
家族一样。我们一般使用RefCell<T>
包裹Rc<T>
以获得一个有内部可变性的共享所有权数据类型。同样的,使用Mutex<T>
可以改变Arc<T>
里面的内容。
当使用Mutex<T>
时,Rust 无法保护您免受各种逻辑错误的影响。使用Rc<T>
会带来创建引用循环的风险,其中两个Rc<T>
值相互引用,从而导致内存泄漏。同样, Mutex<T>
也存在产生死锁(deadlock) 的风险。当一个操作需要锁定两个资源并且两个线程各自获取其中一个锁,导致它们永远等待对方时,就会发生这种情况。Mutex<T>
和MutexGuard
的标准库API文档提供了有用的信息。详见:Mutex<T>
API文档和MutexGuard
API文档。
相关文章:

【Rust自学】16.3. 共享状态的并发
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 16.3.1. 使用共享来实现并发 还记得Go语言有一句名言是这么说的:Do not commun…...
开发者交流平台项目部署到阿里云服务器教程
本文使用PuTTY软件在本地Windows系统远程控制Linux服务器;其中,Windows系统为Windows 10专业版,Linux系统为CentOS 7.6 64位。 1.工具软件的准备 maven:https://archive.apache.org/dist/maven/maven-3/3.6.1/binaries/apache-m…...

【2024年华为OD机试】 (B卷,100分)- 乘坐保密电梯(JavaScriptJava PythonC/C++)
一、问题描述 问题描述 我们需要从0楼到达指定楼层m,乘坐电梯的规则如下: 给定一个数字序列,每次根据序列中的数字n,上升n层或下降n层。前后两次的方向必须相反,且首次方向向上。必须使用序列中的所有数字,不能只使用一部分。目标是到达指定楼层m,如果无法到达,则给出…...

maven的打包插件如何使用
默认的情况下,当直接执行maven项目的编译命令时,对于结果来说是不打第三方包的,只有一个单独的代码jar,想要打一个包含其他资源的完整包就需要用到maven编译插件,使用时分以下几种情况 第一种:当只是想单纯…...
solidity高阶 -- 线性继承
Solidity是一种面向合约的高级编程语言,用于编写智能合约。在Solidity中,多线继承是一个强大的特性,允许合约从多个父合约继承属性和方法。本文将详细介绍Solidity中的多线继承,并通过不同的实例展示其使用方法和注意事项。 在Sol…...
国内外大语言模型领域发展现状与预期
在数字化浪潮中,大语言模型已成为人工智能领域的关键力量,深刻影响着各个行业的发展轨迹。下面我们将深入探讨国内外大语言模型领域的发展现状以及未来预期。 一、发展现状 (一)国外进展 美国的引领地位:OpenAI 的 …...
【Leetcode 热题 100】416. 分割等和子集
问题背景 给你一个 只包含正整数 的 非空 数组 n u m s nums nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 数据约束 1 ≤ n u m s . l e n g t h ≤ 200 1 \le nums.length \le 200 1≤nums.length≤200 1 ≤ n u m s [ i ] ≤ …...

C语言------数组从入门到精通
1.一维数组 目标:通过思维导图了解学习一维数组的核心知识点: 1.1定义 使用 类型名 数组名[数组长度]; 定义数组。 // 示例: int arr[5]; 1.2一维数组初始化 数组的初始化可以分为静态初始化和动态初始化两种方式。 它们的主要区别在于初始化的时机和内存分配的方…...

物管系统赋能智慧物业管理提升服务质量与工作效率的新风潮
内容概要 在当今的物业管理领域,物管系统的崛起为智慧物业管理带来了新的机遇和挑战。这些先进的系统能够有效整合各类信息,促进数字化管理,从而提升服务质量和工作效率。通过物管系统,物业管理者可以实时查看和分析各种数据&…...

2024年记 | 凛冬将至
放弃幻想,准备斗争! 考研or就业? 上大学以来,考研上名校在我的心里一直是一颗种子,2024年初,当时的想法是考研和就业两手抓。买了张宇的高数现代,想要死磕! 也记了挺多笔记... 如果…...
MySQL数据导入与导出
在现代软件开发中,数据管理是一个重要的核心环节,而数据库则是进行数据管理的主要工具。MySQL 作为一款开源的关系型数据库管理系统,被广泛应用于企业和个人开发项目中。对于学习编程的初学者或是自学者来说,掌握 MySQL 的基本操作尤为重要,尤其是数据的导入与导出功能。这…...

NoSQL与SQL比较
1.认识NoSQL NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库。 1.1.结构…...
Ceph:关于Ceph 中使用 RADOS 块设备提供块存储的一些笔记整理(12)
写在前面 准备考试,整理 ceph 相关笔记博文内容涉及使用 RADOS 块设备提供块存储理解不足小伙伴帮忙指正对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波…...
Android SystemUI——最近任务列表启动(十八)
前面分析了初始化涉及到的关键类,系统启动后会启动 SystemUI 进程,然后进行一系列初始化,接下来看一下进入 Recents 的流程。我们主要分析最近任务应用列表的启动与显示。 一、最近任务启动 关于手势或 Key 按键触发这一块逻辑处理入口都是在 PhoneWindowManager,咱们从 R…...

数据结构课程设计(三)构建决策树
3 决策树 3.1 需求规格说明 【问题描述】 ID3算法是一种贪心算法,用来构造决策树。ID3算法起源于概念学习系统(CLS),以信息熵的下降速度为选取测试属性的标准,即在每个节点选取还尚未被用来划分的具有最高信息增益的…...

从ChatGPT热潮看智算崛起
2025年1月7日,科智咨询发布《2025年IDC产业七大发展趋势》,其中提到“ChatGPT开启生成式AI热潮,智能算力需求暴涨,算力供给结构发生转变”。 【图片来源于网络,侵删】 为何会以ChatGPT发布为节点呢?咱们一起…...
基于PyQt设计的智能停车管理系统
文章目录 一、前言1.1 项目介绍【1】项目开发背景【2】设计实现的功能【3】设计意义【4】国内外研究现状【6】摘要1.2 设计思路1.3 系统功能总结1.4 开发工具的选择【1】VSCODE【2】python【3】ptqt【4】HyperLPR31.5 参考文献二、安装Python环境1.1 环境介绍**1.2 Python版本介…...

http的请求体各项解析
一、前言 做Java开发的人员都知道,其实我们很多时候不单单在写Java程序。做的各种各样的系统,不管是PC的 还是移动端的,还是为别的系统提供接口。其实都离不开http协议或者https 这些东西。Java作为编程语言,再做业务开发时&#…...
【linux】Linux 常见目录特性、权限和功能
目录特性默认权限主要功能/用途/根目录,所有目录的起点755文件系统的顶层目录,包含所有其他子目录和文件/bin基础二进制命令目录(系统启动和修复必需的命令)755存放所有用户可用的基本命令(如 ls, cp, bash 等…...

创作三载·福启新章2025
写在前面:本博客仅作记录学习之用,部分图片来自网络,如需引用请注明出处,同时如有侵犯您的权益,请联系删除! 文章目录 前言机缘收获日常憧憬 总结 前言 在2022年01月26日,我踏上了技术创作的征…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...

wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...