【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实现了Dereftrait,我们就可以获得内部数据的引用。所以num是一个可变引用。 - 在小作用域内还使用了解引用
*来修改数据的值为6。 - 由于
MutexGuard实现了Droptrait,所以在小作用域结束后会自动解锁。 - 最后打印了修改后的互斥锁内的内容。
输出:
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文档和MutexGuardAPI文档。
相关文章:
【Rust自学】16.3. 共享状态的并发
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 16.3.1. 使用共享来实现并发 还记得Go语言有一句名言是这么说的:Do not communicate by sharing memory; instead, share me…...
git 新项目
新项目git 新建的项目如何进行git 配置git git config --global user.name "cc" git config --global user.email ccexample.com配置远程仓库路径 // 添加 git remote add origin http://gogs/cc/mc.git //如果配错了,删除 git remote remove origin初…...
【LeetCode 刷题】回溯算法-子集问题
此博客为《代码随想录》二叉树章节的学习笔记,主要内容为回溯算法子集问题相关的题目解析。 文章目录 78.子集90.子集II 78.子集 题目链接 class Solution:def subsets(self, nums: List[int]) -> List[List[int]]:res, path [], []def dfs(start: int) ->…...
LLMs之DeepSeek:Math-To-Manim的简介(包括DeepSeek R1-Zero的详解)、安装和使用方法、案例应用之详细攻略
LLMs之DeepSeek:Math-To-Manim的简介(包括DeepSeek R1-Zero的详解)、安装和使用方法、案例应用之详细攻略 目录 Math-To-Manim的简介 1、特点 2、一个空间推理测试—考察不同大型语言模型如何解释和可视化空间关系 3、DeepSeek R1-Zero的简介:处理更…...
2025年2月2日(网络编程 tcp)
tcp 循环服务 import socketdef main():# 创建 socket# 绑定tcp_server socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_server.bind(("", 8080))# socket 转变为被动tcp_server.listen(128)while True:# 产生专门为链接进来的客户端服务的 socketprint(&qu…...
WSL2中安装的ubuntu搭建tftp服务器uboot通过tftp下载
Windows中安装wsl2,wsl2里安装ubuntu。 1. Wsl启动后 1)Windows下ip ipconfig 以太网适配器 vEthernet (WSL (Hyper-V firewall)): 连接特定的 DNS 后缀 . . . . . . . : IPv4 地址 . . . . . . . . . . . . : 172.19.32.1 子网掩码 . . . . . . . .…...
C#从XmlDocument提取完整字符串
方法1:通过XmlDocument的OuterXml属性,见XmlDocument类 该方法获得的xml字符串是不带格式的,可读性差 方法2:利用XmlWriterSettings控制格式等一系列参数,见XmlWriterSettings类 例子: using System.IO; …...
Ubuntu 下 nginx-1.24.0 源码分析 main函数 — ngx_cdecl 宏
ngx_cdecl 宏 int ngx_cdecl main(int argc, char *const *argv) ngx_cdecl 定义在: ngx_config.h 中: #define ngx_cdecl 这里是一个空的 define 参考: nginx中的ngx_cdecl-CSDN博客 __cdecl 是一种调用约定(Calling Con…...
2025-工具集合整理
科技趋势 github-rank 🕷️Github China/Global User Ranking, Global Warehouse Star Ranking (Github Action is automatically updated daily). 科技爱好者周刊 制图工具 D2 D2 A modern diagram scripting language that turns text to diagrams 文档帮助 …...
OpenAI 实战进阶教程 - 第一节:OpenAI API 架构与基础调用
目标 掌握 OpenAI API 的基础调用方法。理解如何通过 API 进行内容生成。使用实际应用场景帮助零基础读者理解 API 的基本用法。 一、什么是 OpenAI API? OpenAI API 是一种工具,允许开发者通过编程方式与 OpenAI 的强大语言模型(例如 gpt-…...
Alibaba开发规范_编程规约之集合框架:最佳实践与常见陷阱
文章目录 引言1. hashCode与equals方法的覆写1.1 规则1.2 解释1.3 代码示例正例反例 2. ArrayList的subList方法2.1 规则2.2 解释2.3 代码示例正例反例 3. Map的keySet、values和entrySet方法3.1 规则3.2 解释3.3 代码示例正例反例 4. Collections类返回的不可变集合4.1 规则4.…...
NPM 使用介绍
NPM 使用介绍 引言 NPM(Node Package Manager)是Node.js生态系统中的一个核心工具,用于管理JavaScript项目的依赖包。无论是开发一个小型脚本还是构建大型应用程序,NPM都能极大地提高开发效率。本文将详细介绍NPM的使用方法,包括安装、配置、依赖管理、包发布等,帮助您…...
小红的小球染色期望
B-小红的小球染色_牛客周赛 Round 79 题目描述 本题与《F.R小红的小球染色期望》共享题目背景,但是所求内容与范围均不同,我们建议您重新阅读题面。 有 n 个白色小球排成一排。小红每次将随机选择两个相邻的白色小球,将它们染成红色。小红…...
基于SpringBoot的新闻资讯系统的设计与实现(源码+SQL脚本+LW+部署讲解等)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
计算机网络——流量控制
流量控制的基本方法是确保发送方不会以超过接收方处理能力的速度发送数据包。 通常的做法是接收方会向发送方提供某种反馈,如: (1)停止&等待 在任何时候只有一个数据包在传输,发送方发送一个数据包,…...
基于python的Kimi AI 聊天应用
因为这几天deepseek有点状况,导致apikey一直生成不了,用kimi练练手。这是一个基于 Moonshot AI 的 Kimi 接口开发的聊天应用程序,使用 Python Tkinter 构建图形界面。 项目结构 项目由三个主要Python文件组成: 1. main_kimi.py…...
2 [GitHub遭遇严重供应链投毒攻击]
近日,有黑客针对 Discord Top.gg 的GitHub 账户发起了供应链攻击,此次攻击导致账户密码、凭证和其他敏感信息被盗,同时也影响到了大量开发人员。 Checkmarx 在一份技术报告中提到,黑客在这次攻击中使用了多种TTP,其中…...
C++游戏开发实战:从引擎架构到物理碰撞
📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 1. 引言 C 是游戏开发中最受欢迎的编程语言之一,因其高性能、低延迟和强大的底层控制能力,被广泛用于游戏…...
代码讲解系列-CV(一)——CV基础框架
文章目录 一、环境配置IDE选择一套完整复现安装自定义cuda算子 二、Linux基础文件和目录操作查看显卡状态压缩和解压 三、常用工具和pipeline远程文件工具版本管理代码辅助工具 随手记录下一个晚课 一、环境配置 pytorch是AI框架用的很多,或者 其他是国内的框架 an…...
【前端知识】常用CSS样式举例
文章目录 一、Flex盒子布局1. Flexbox 的基本概念2. Flex 容器的属性2.1 display2.2 flex-direction2.3 flex-wrap2.4 justify-content2.5 align-items2.6 align-content 3. Flex 项目的属性3.1 order3.2 flex-grow3.3 flex-shrink3.4 flex-basis3.5 flex3.6 align-self 4. 示例…...
P_all: 投影矩阵(Projection Matrix)
P_all 是所有摄像头的投影矩阵(Projection Matrix)的集合。每个摄像头的投影矩阵 Pi 是一个 34 的矩阵,用于将世界坐标系中的 3D 点 X[X,Y,Z,1]T 投影到该摄像头的 2D 图像平面上的点 u[u,v,1]T。投影关系可以表示为: uPiX 其中…...
机器学习--概览
一、机器学习基础概念 1. 定义 机器学习(Machine Learning, ML):通过算法让计算机从数据中自动学习规律,并利用学习到的模型进行预测或决策,而无需显式编程。 2. 与编程的区别 传统编程机器学习输入:规…...
Python算法详解:贪心算法
贪心算法(Greedy Algorithm)是一种通过选择当前最优解以期望达到全局最优解的算法思想。它在每一步选择时只考虑当前状态下的局部最优,而不关心全局问题的复杂性。这种算法简单高效,适用于某些特定问题,尤其是存在贪心…...
gesp(C++六级)(10)洛谷:P10722:[GESP202406 六级] 二叉树
gesp(C六级)(10)洛谷:P10722:[GESP202406 六级] 二叉树 题目描述 小杨有⼀棵包含 n n n 个节点的二叉树,且根节点的编号为 1 1 1。这棵二叉树任意⼀个节点要么是白色,要么是黑色。之后小杨会对这棵二叉树…...
7.DP算法
DP 在C中,动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题来高效求解的算法设计范式。以下是DP算法的核心要点和实现方法: 一、动态规划的核心思想 重叠子问题:问题可分解为多个重…...
2025年2月2日(tcp3次握手4次挥手)
TCP(三次握手和四次挥手)是建立和关闭网络连接的标准过程,确保数据在传输过程中可靠无误。下面是详细解释: 1. 三次握手(TCP连接建立过程) 三次握手是为了在客户端和服务器之间建立一个可靠的连接&#x…...
w186格障碍诊断系统spring boot设计与实现
🙊作者简介:多年一线开发工作经验,原创团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取,记得注明来意哦~🌹赠送计算机毕业设计600个选题excel文…...
Android Studio 正式版 10 周年回顾,承载 Androider 的峥嵘十年
Android Studio 1.0 宣发于 2014 年 12 月,而现在时间来到 2025 ,不知不觉间 Android Studio 已经陪伴 Androider 走过十年历程。 Android Studio 10 周年,也代表着了我的职业生涯也超十年,现在回想起来依然觉得「唏嘘」ÿ…...
【PyQt】lambda函数,实现动态传递参数
为什么需要 lambda? 在 PyQt5 中,clicked 信号默认会传递一个布尔值(表示按钮是否被选中)。如果我们希望将按钮的文本内容传递给槽函数,需要通过 lambda 函数显式传递参数。 这样可以实现将按钮内容传递给槽函数&…...
4 Hadoop 面试真题
4 Hadoop 面试真题 1. Apache Hadoop 3.0.02. HDFS 3.x 数据存储新特性-纠删码Hadoop面试真题 1. Apache Hadoop 3.0.0 Apache Hadoop 3.0.0在以前的主要发行版本(hadoop-2.x)上进行了许多重大改进。 最低要求的Java版本从Java 7增加到Java 8 现在&…...
