2311rust无畏并发.

原文

Rust无畏并发

Rust是为了解决两个麻烦问题:
1,如何安全系统编程
2,如何无畏并发

最初,这些问题似乎是无关的,但令惊讶的是,方法竟然是相同的:使Rust安全的相同工具也可帮助解决并发问题.

内存安全和并发错误,一般认为是代码在不应访问数据时访问数据.Rust依靠所有权为你静态检查.

内存安全,即可在无垃集时编程,也不必担心段错误,因为Rust会发现你的错误.

并发性,即可从(传递消息,共享状态,无锁,纯函数式)中选择,而Rust帮助你避免常见的陷阱.

以下是Rust中的并发性:

1,通道转移了发送消息的所有权,因此可从一个线程发送指针到另一个线程,而不必担心线程竞争.Rust通道强制隔离线程.这里

2,知道它保护了哪些数据,且Rust保证,只有在持有锁时,才能访问数据.而不会共享状态.在Rust中强制"锁定数据,而不是代码".

3,在多线程之间,每种数据类型都知道它是否可安全发送或访问,且Rust强制,即使对无锁数据结构,也无数据竞争.线安不仅是文档;也是规则.

4,甚至可在线程共享栈帧这里,Rust静态地确保,在其他线程使用它们时,这些帧活跃.即使是最大胆的共享形式,在Rust中也能保证安全.

这些好处都来自Rust所有权模型,事实上,锁,通道,无锁数据结构等都是在库中而不是核心语言中定义的.
Rust并发方法是开放的:新库可带有新的范式并抓新的错误,只需添加使用Rust所有权功能的API.

背景:所有权

Rust中,每个值都有个"物主域",传递或返回值表明从旧所有权转移("移动")到新域.在结束时,此时自动析构仍拥有的值.
看看简单示例.假设创建一个向量并推送一些元素到它上面:

fn make_vec() {let mut vec = Vec::new();//归`make_vec`的域所有vec.push(0);vec.push(1);//域结束,析构`"vec"`
}

创建值的域最初也拥有它.此时,make_vec的主体是vec的物主域.物主可用vec干活.
在域结束时,仍归域所有vec,因此会自动释放.
如果返回或传递向量,会更有趣:

fn make_vec() -> Vec<i32> {let mut vec = Vec::new();vec.push(0);vec.push(1);vec //转让`所有权`给调用者
}
fn print_vec(vec: Vec<i32>) {//`"vec"`参数是此域的一部分,因此归`"print_vec"`所有for i in vec.iter() {println!("{}", i)}//现在,释放`"vec"`
}
fn use_vec() {let vec = make_vec(); //取向量所有权,print_vec(vec);       //传递所有权给`"print_vec"`
}

现在,在make_vec域结束前,vec返回它来出域;不会析构它.然后,像use_vec此调用者获得向量所有权.
另一方面,print_vec函数带vec参数,由其调用者把向量的所有权转移给它.因为print_vec不会进一步转移所有权,因此在其域结束时,就析构向量.
一旦放弃所有权,就不能再使用该值.如,请考虑以下use_vec变体:

fn use_vec() {let vec = make_vec();  //取`VectorPass`所有权print_vec(vec);        //传递所有权给`"print_vec"`,for i in vec.iter() {  //继续使用`"vec"`println!("{}", i * 2)}
}

编译器说不再可用vec;已转移所有权.这非常好,因此时已释放了向量!避免了灾难.

借贷

目前,并不满意,因为无意print_vec析构向量.真正想要的是临时授予print_vec访问向量,然后继续使用向量.

这就要靠借贷了.如果有权访问Rust中的某个值,可把该权限借给调用函数.Rust检查这些生命期不会超过被借对象.
借用一个值,可用&符号引用它(一个指针):

fn print_vec(vec: &Vec<i32>) {//`"vec"`参数是`此域`借用的for i in vec.iter() {println!("{}", i)}//现在,借期结束了
}
fn use_vec() {let vec = make_vec();  //取向量的所有权print_vec(&vec);       //借出`"print_vec"`权限for i in vec.iter() {  //继续使用`"vec"`println!("{}", i * 2)}//在此析构`VEC`
}

现在print_vec接受向量引用,use_vec通过编写&vec来借出向量.因为是临时的,use_vec保留了向量所有权;

可在调用print_vec返回后继续使用它.

每个引用有限域内有效,编译器自动确定该域.有两种引用形式:

1,不变引用&T,允许共享禁止改变.可同时有多个对同一值的&T引用,但当这些引用活动时,不能更改该值.
2,可变引用&mut T,允许改变不共享.如果存在对某个值的&mut T引用,则此时不能有其他活动引用,但可更改该值.

Rust在编译时检查这些规则;借用没有运行时成本.
为什么有两类引用?考虑此函数:

fn push_all(from: &Vec<i32>, to: &mut Vec<i32>) {for i in from.iter() {to.push(*i);}
}

此函数遍历向量的每个元素,把它推送另一个向量上.迭代器在当前和最终位置保持向量指针,挨个前进.
如果用相同向量,为两个参数调用此函数怎么办?

push_all(&vec, &mut vec)

这将是一场灾难!推送元素到向量上时,它偶尔要调整,分配大量新内存并复制进元素.迭代器会剩下旧内存指针的悬挂指针,导致内存不安全(段错误则更糟).

幸好,Rust确保每当可变借用活动时,其他借用都不会活动,从而产生以下消息:
错误:不能按可变借用"vec",因为它也按不变借用.

push_all(&vec, &mut vec);^~~

传递消息

并发编程有多种风格,特别简单方式是线程或参与者相互发送消息来通信的传递消息.
不通过共享内存交流;相反,通过交流共享内存.

Rust所有权使得很容易检查规则.考虑以下通道API(Rust标准库中的通道略有不同):

fn send<T: Send>(chan: &Channel<T>, t: T);
fn recv<T: Send>(chan: &Channel<T>) -> T;

通道在它们传输的数据类型(API<T:Send>部分)上是通用的.Send部分表明T必须是安全的,可在线程之间发送;
Vec<i32>Send.

Rust中一样,传递Tsend函数表明转移它的所有权.这一事实深远影响:即,下面代码生成编译器错误.

//假设`chan:Channel<Vec<i32>>`
let mut vec = Vec::new();
//做一些计算
send(&chan, vec);
print_vec(&vec);

在此,线程创建了一个向量,并发送它到另一个线程,然后继续使用它.当该线程继续运行时,接收向量线程可能会更改它,因此调用print_vec,可能会导致竞争,因此,导致释放后使用错误.

相反,在调用print_vec时,Rust编译器会生成错误消息:
错误:使用移动"vec"值.
避免了灾难.

锁,被动共享状态来通信的方式.
共享状态并发有个缺点.很容易忘记取锁,或在错误时间改变错误数据,导致灾难.

Rust的观点是:
然而,共享状态并发是基本编程风格,系统代码,最大性能及实现其他并发风格都需要它.
问题与意外共享状态有关.

无论使用有锁还是无锁技术,Rust旨在为你提供直接征服共享状态并发的工具.

Rust中,因为所有权,线程会自动相互"隔离".无论是拥有数据,还是可变借用数据,仅当线程可变权限时,才会写入.

总之,保证该线程是当时唯一有权限的线程.
请记住,不能同时有可变借用与其他借用.锁通过运行时同步提供相同的保证("互斥").这导致直接勾挂到Rust所有权系统的锁API.
如下是简化版本:

//创建新的互斥锁
fn mutex<T: Send>(t: T) -> Mutex<T>;
//取锁
fn lock<T: Send>(mutex: &Mutex<T>) -> MutexGuard<T>;
//访问受锁保护的数据
fn access<T: Send>(guard: &mut MutexGuard<T>) -> &mut T;

此锁API的不寻常点.
1,首先,在锁保护数据T类型上,互斥类型通用的.创建互斥锁时,转移该数据所有权到互斥锁中,立即放弃了所有权.(在首次创建时解锁).
2,稍后,你可锁(lock)阻止线程,直到获得.在析构MutexGuard时自动释放锁;没有单独的解锁(unlock)函数.
3,只能通过访问(access)函数访问锁,该函数把守卫可变借用转换为数据的可变借用(短期借用):

fn use_lock(mutex: &Mutex<Vec<i32>>) {//获得锁,拥有警卫;在域的其余部分持有锁let mut guard = lock(mutex);//通过可变借用`Guard`来访问数据let vec = access(&mut guard);//`vec`的类型为`"&mut Vec<i32>"`vec.push(3);//析构`"守卫"`时,会自动在此处释放锁
}

两个关键要素:
1,访问(access)返回的可变引用不能超过比它借用的MutexGuard.
2,仅当析构MutexGuard时,才会释放锁.

结果是Rust强制保证锁规则:除非持有锁,否则禁止访问受锁保护数据.否则生成编译器错误.如,考虑以下有缺陷的"重构":

fn use_lock(mutex: &Mutex<Vec<i32>>) {let vec = {//取锁let mut guard = lock(mutex);//试返回借用数据access(&mut guard)//在此析构`守卫`,释放了锁};//试访问锁外数据.vec.push(3);
}

Rust生成错误来说明问题:
错误:"guard"的生命期不够长

access(&mut guard)^~~~~

避免了灾难.

线安和"发送"

一般区分某些数据类型为"线安",而其他数据类型则不是.线安数据结构内部有足够同步,以便可同时安全地使用多线程.
如,Rust附带了两个来引用计数的"灵针":
1,Rc<T>通过正常读/写提供引用计数.它不是线安的.
2,Arc<T>通过原子操作提供引用计数.它是线安的.

Arc使用的硬件原子操作比Rc使用的普通操作更贵,因此使用Rc而不是Arc是有利的.另一方面,重点,永远不要从一个线程迁移Rc<T>到另一个线程,因为会导致破坏引用计数的竞争.

Rust中,世界分为两个数据类型:一个是Send,即可安全地从一个线程移动到另一个线程,其余是!Send(不安全).

如果某个类型的所有组件都是Send,则该类型也是Send,它涵盖了大多数类型.但是,某些基本类型不是线安的,因此也可按Send显式标记Arc等类型,对编译器说:相信我;已在此验证了必要的同步.

当然,ArcSend,而Rc不是.

可见,通道和互斥API仅适合发送(Send)数据.因为它们是跨越线程边界的数据点,因此它们也是Send强制点.

综上,Rust可自信地获得Rc和其他线程不安全类型的好处,因为,如果不小心试发送一个线程到另一个线程,Rust编译器会说:
无法安全地在线程之间发送"Rc<Vec<i32>>".
这避免了灾难.

共享栈:"scoped"

注意:这里提到的API是一个旧的API,已从标准库中移出.你可在横梁(scope()文档)和scoped_threadpool(scoped()文档)中找到等效的函数.

目前,所有模式都涉及在上创建,在线程共享的数据结构.但是,如果想启动一些线程来利用栈帧中的数据,则可能会很危险:

fn parent() {let mut vec = Vec::new();//填充向量thread::spawn(|| {print_vec(&vec)})
}

子线程接受vec引用,而vec保留在父线程的栈帧中.父线程退出时,会弹出栈帧,但子线程并不知道.哎呀!

为了排除该内存不安全,Rust的基本线程生成API如下:

fn spawn<F>(f: F) where F: 'static, ...

"静态约束"即,指在闭包禁止借用数据.即像上面此parent函数会生成错误:
错误:"vec"的生命期不够长.

基本上抓住了弹出父栈帧的可能性.避免了灾难.

还有另一个方法可保证安全性:直到子线程完成,确保父栈帧保持原位.这是分叉连接编程的模式,一般用于分而治之的并行算法.
Rust通过提供线程生成的"域"变体来支持它:

fn scoped<'a, F>(f: F) -> JoinGuard<'a> where F: 'a, ...

与上面的spawn接口有两个主要区别:
1,使用'a参数,而不是'static.
2,JoinGuard返回值.即,JoinGuard通过在其析构器隐式连接(如果尚未显式)来确保父线程加入(等待)其子线程.

JoinGuard中包含'a可确保JoinGuard无法逃脱闭包借用的数据的域.即,Rust保证在弹出子线程可能访问的栈帧前,父线程等待子线程完成.

因此,调整之前示例,可如下修复错误并满足编译器:

fn parent() {let mut vec = Vec::new();//填充向量let guard = thread::scoped(|| {print_vec(&vec)});//在此析构`守卫`,隐式合并
}

因此,在Rust中,可自由地把栈数据借用到子线程中,编译器确保检查是否有足够同步.

数据竞争

Rust使用所有权和借用来保证:
1,内存安全,无垃集.
2,无并发数据竞争.

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

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

相关文章

删除word最后一页之后的空白页

最近编辑word比较多&#xff0c;有时最后一页&#xff08;最后一页内容还有可能是表格&#xff09;之后&#xff0c;还有一页空白页&#xff0c;单独按下backspace、del都删不掉&#xff0c;很让人着急。 经过查询有几种方法&#xff1a; &#xff08;1&#xff09;点击选中空…

12、填写NGINX配置部署前端;运行jar部署后端

后端可以部署的方式&#xff0c;首先直接运行jar是肯定可以的。此外&#xff0c;可以单独开docker容器运行在容器中。 但是这里运行在容器中必要性&#xff0c;其实并不大。 当前我们直接运行jar来运行后端。后面推出集成docker。 直接运行jar包的方式&#xff0c;首先需要打…

Spark Streaming

Spark Streaming Spark Streaming概念Spark Streaming操作1 netcat传入数据2 DStream 创建3 自定义数据源4 接受kafka数据DStream 转换1无状态的转换2有状态的转换updateSateByKeyWindowOperations Spark Streaming概念 Spark Streaming 用于流式数据的处理。 Spark Streaming…

临界资源,临界区,通信的干扰问题(互斥),信号量(本质,上下文切换问题,原子性,自身的安全性,操作)

目录 引入 概念 临界资源 临界区 干扰存在原因 互斥 信号量 引入 举例 概念 介绍 表示可用资源数 表示等待进程数 申请信号量 信号量的本质 全局变量? 共享内存? 不安全问题 -- 上下文切换 原子性 信号量自身的安全性 原子操作的意义 操作 引入 通信…

Collection集合 迭代器遍历Iterator 和集合增强For

迭代器遍历Iterator 标准写法: 增强For for(类型 名称 : 集合 ) 举例: 不仅可以集合也可以数组 底层仍然是iterator

【MySQL】约束

一、基本概念 1、什么是约束 约束是表级的强制规定 2、为什么使用约束 是为了保证表中数据的完整性&#xff0c;完整性又可以拆分为精确性和可靠性 3、怎么去保证数据完整性呢&#xff0c;从以下四个角度进行考虑 实体完整性&#xff1a;一张表中&#xff0c;不能存在两条完…

Power Apps-库组件连接数据表

点击添加数据 可以选择Excel或SharePoint导入 选择右侧边栏中的网站&#xff0c;再选择想要连接的数据表 点击插入&#xff0c;选择布局中的某个库&#xff0c; 选中它可以点击上方的布局&#xff0c;选择想要的样式 右侧选择数据源中的表就将组件与数据表连接起来了 如果想修…

Vite创建React项目,另外一种更加简单的方法

在上一篇blog中一个一个安装依赖dependencies&#xff0c;有没有一步到位的方法呢&#xff0c;有! 参考《React 18 Design Patterns and Best Practices Design, build, and deploy production-ready web applications with React》4th 第一章倒数第二节Vite as a solution有个…

flutter生态一统甜夏 @Android @ios @windowse @macos @linux @Web

(愿景)G o o g l e 中 国flutter生态一统天下(IT) Web Android ios Windowse Macos Linux Google中国https://space.bilibili.com/64169458 https://pub-web.flutter-io.cn 构建 Flutter Web 应用 构建 Flutter Web 应用 - Flutter 中文文档 - Flutter 中文开发者网站 …

Packet Tracer路由器连接终端设备怎么配置?

在Packet Tracer中配置一台路由器和三台终端设备可以帮助你建立一个简单的局域网&#xff0c;以下是配置的基本步骤&#xff1a; 打开Packet Tracer&#xff0c;从左侧设备栏中拖拽一个路由器和三个终端设备到工作区。 连接设备&#xff1a;使用网线将路由器的端口与每台终端设…

vue3+setup 解决:this.$refs引用子组件报错 is not a function

一、如果在父组件中以下四步都没问题的话&#xff0c;再看下面步骤 二、如果父组件引用的是index页面 请在 头部加上以下代码 &#xff08;如果是form页面请忽略这一步&#xff09; <template> <a-modalv-model:visible"visible"title"头部名称&…

python非线性规划

Python中非线性规划通常使用优化库来处理&#xff0c;其中SciPy库是一个流行的选择。SciPy包含了用于非线性规划的优化算法&#xff0c;可以用来解决各种非线性优化问题。下面是一个简单的非线性规划的示例&#xff0c;使用SciPy来最小化一个非线性目标函数&#xff1a; 首先&a…

SpringCloud 微服务全栈体系(十三)

第十一章 分布式搜索引擎 elasticsearch 二、索引库操作 索引库就类似数据库表&#xff0c;mapping 映射就类似表的结构。 我们要向 es 中存储数据&#xff0c;必须先创建“库”和“表”。 1. mapping 映射属性 mapping 是对索引库中文档的约束&#xff0c;常见的 mapping …

SpringDataJpa(二)

三、Spring Data JPA概述 Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架&#xff0c;可使开发者用极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常用功能&#xff0c;且易于扩展&#xff01;学习并使用 Spring D…

汽车标定技术(五)--基于模型开发如何生成完整的A2L文件(1)

1 数据对象的创建 CtrlH打开Model Explorer&#xff0c;在Base workspace中点击工具栏add&#xff0c;出现如下界面&#xff0c; 可以看到Simulink提供了多种数据类型 Matlab Variable&#xff1a;Simulink.Parameter&#xff1a;使用该数据对象表示工程应用中的标定量Simuli…

js:React中使用classnames实现按照条件将类名连接起来

参考文档 https://www.npmjs.com/package/classnameshttps://github.com/JedWatson/classnames 安装 npm install classnames示例 import classNames from "classnames";// 字符串合并 console.log(classNames("foo", "bar")); // foo bar//…

安卓常见设计模式6------代理模式(Kotlin版)

1. W1 是什么&#xff0c;什么是代理模式&#xff1f;​ 代理模式&#xff08;Proxy Pattern&#xff09;是一种结构型设计模式&#xff0c;用于在访问对象之前或之后提供额外的功能或控制。代理模式可以用于各种情况&#xff0c;例如延迟加载、权限控制、日志记录等。​ 2. …

高性能网络编程 - The C10M problem

文章目录 Pre概述回顾C10K实现C10M的挑战思路总结 Pre 高性能网络编程 - The C10K problem 以及 网络编程技术角度的解决思路 概述 在接下来的10年里&#xff0c;因为IPv6协议下每个服务器的潜在连接数都是数以百万级的&#xff0c;单机服务器处理数百万的并发连接&#xff0…

基于单片机智能加湿器控制系统仿真设计

**单片机设计介绍&#xff0c; 698【毕业课设】基于单片机智能加湿器控制系统仿真设计 文章目录 一 概要系统组成总结 二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 单片机智能加湿器控制系统仿真设计介绍 单片机智能加湿器控制系统是一种利用微…

Jakarta-JVM篇

文章目录 一.前言1. 1 JVM-堆常用调参1.2 JVM-方法区常用参数1.3 JVM-codeCache 二.JVM内存结构三. 对象创建四. JVM垃圾回收算法4.1 可达性分析算法4.1.1 对象引用4.1.2 回收方法区. 4.2 分代回收4.3 标记清除4.4 标记复制4.5 标记整理 五.垃圾回收器5.1 根节点枚举5.2 安全点…