一文学会Rust?
Rust是什么
Rust 是一个系统编程语言,它注重三个方面:安全,速度和并发性。
特征:
1.没有垃圾回收机制,没有运行时,效率超过c++,直逼c语言
2.内存安全,并发安全,没有空指针
3.极其丰富的生态 https://github.com/rust-lang/rust ,https://crates.io/
它是如何做到这些的?
编译时保证和对内存生命周期的明确控制。
让我们来谈谈Rust中最重要的概念:“所有权”,以及它对并发编程(对程序员来讲通常是非常困难的任务)的启发。
所有权
所有权是Rust的核心概念,也是其独特的功能之一。 “所有权”是指允许哪部分的代码修改内存。
让我们从一些C++代码开始理解这个概念:
int *foo(void)
{
int i = ;
return &i;
} int bar(void)
{
int *num = foo();
return *num + ;
}
foo函数在栈上分配了一个整型,然后保存给一个变量i,最后返回了这个变量i的引用。这里有一个问题:当函数返回时栈内存变成失效。意味着在函数add_one第二行,指针num指向了垃圾值,我们将无法得到想要的结果。虽然这个一个简单的例子,但是在C++的代码里会经常发生。当堆上的内存使用malloc(或new)分配,然后使用free(或delete)释放时,会出现类似的问题,但是您的代码会尝试使用指向该内存的指针执行某些操作。 更现代的C ++使用RAII和构造函数/析构函数,但它们无法完全避免“野指针”。
Rust就可以不出现这种情况。 我们试试吧:
fn foo() -> &i8 {
let i = 1234;
return &i;
} fn bar() -> i8 {
let num = foo();
return *num + 1;
}
当你尝试编译这个程序时,你会得到一个有意思的错误信息:
rustc main.rs
error[E0106]: missing lifetime specifier
--> main.rs:1:13
|
1 | fn foo() -> &i8 {
| ^ help: consider giving it a 'static lifetime: `&'static`
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
为了完全理解这个错误信息,我们需要谈谈“拥有”某些东西意味着什么。 所以现在,让我们接受Rust不允许我们用野指针编写代码,一旦我们理解了所有权,我们再回头看这块代码。
我们先打个比方。 我喜欢读书,有时候我真的很喜欢一本书,会推荐给我的朋友们来读。 当我读我的书时,我拥有它:这本书是我所拥有的。 当我把书”借“给你,在特定的一段时间它是属于你的,然后你把它还给我,我又拥有它了。 对吗?
这个概念也直接应用于Rust代码:一些代码“拥有”一个指向内存的特定指针。 它是该指针的唯一所有者。 它还可以暂时将该内存借给其他代码:这些代码“借”了它。 借用它的这一段时间,称为“生命周期”。
这是关于所有权的所有。 那似乎并不那么难,对吧? 让我们回到那条错误信息:error: borrowed value does not live long enough。 我们试图使用Rust的借用指针&,借出一个特定的变量i。 但Rust知道函数返回后该变量无效,因此它告诉我们:
missing lifetime specifier :borrowed pointer must be valid for the anonymous lifetime # ... but borrowed value is only valid for the block。
完美!
Rust能够推断出类型的大小,然后分配正确的内存大小并将其设置为您要求的值。 这意味着你无法分配未初始化的内存:Rust没有null的概念。万岁!
Rust和C ++之间还有另外一个区别:Rust编译器还计算了i的生命周期,然后在它无效后插入相应的free调用,就像C ++中的析构函数一样。 您可以获得手动分配堆内存的所有好处,而无需自己完成所有工作。 此外,所有这些检查都是在编译时完成的,因此没有运行时开销。这意味着如果你编写了正确的C ++代码,你将编写出与C++代码一样的Rust代码,而且由于编译器的帮忙,编写错误的代码在编译时就不会通过。
你已经看到了一种情况,所有权和生命周期有利于防止在别的语言中通常会出现的危险代码。
现在让我们谈谈另一种情况:并发。
并发
并发是当前软件世界中一个令人头疼的热门话题。 对于计算机科学家来说,它一直是一个有趣的研究领域。随着互联网的使用爆炸式增长,人们希望现有的条件可以服务更多的用户数量。 并发是实现这一目标的方式。 但并发代码有一个很大的缺点:它很难调试,因为它是非确定性的。 编写好的并发代码有几种不同的方法,但让我们来谈谈Rust的所有权和生命周期的概念如何帮助实现正确并且并发的代码。
首先,让我们看一下Rust中的简单并发示例。 Rust允许你启动task,这是轻量级的“绿色”线程(协程)。 这些任务没有任何共享内存,因此,我们使用“通道”在task之间进行通信。 像这样:
use std::thread;
use std::sync::mpsc; fn main() { let numbers = [,,];
let (tx, rx) = mpsc::channel(); for _ in .. {
// Create a new owned reference:
let tx = tx.clone();
// Use it in a thread:
thread::spawn(move || { tx.send(numbers).unwrap();
});
} for _ in .. {
let numbers = rx.recv().unwrap();
println!("{}", numbers[0]);
}
}
在这个例子中,我们创建了一个数字数组(vector), 然后我们创建一个channel,这是Rust实现通道的方法,这将返回通道的两个不同端:发送端(tx)和接收端(rx)。spawn函数可以启动一个task。 正如你在代码中看到的那样,我们在task中调用tx.send(),传入vector,我们在外面调用rx.recv()。 然后打印vector的第一个元素。
Rust在通过channel发送前copy了数据, 这样即使它发送的数据是可变的,由于每个线程有自己的一份数据,就不会出现竞争。怎么来证明这里的对象copy了呢?我们修改一下代码:
use std::thread;
use std::sync::mpsc; fn main() { let mut numbers = [,,];
let (tx, rx) = mpsc::channel(); for i in .. {
let tx = tx.clone(); thread::spawn(move || {
numbers[0] += i;
tx.send(numbers).unwrap();
});
} for _ in .. {
let numbers = rx.recv().unwrap();
println!("{:?}", numbers);
}
}
注意看红字的部分,如果这个numbers是同一份对象,那么在累加之后打印的结果应该是一直递增的数,如果是多份,那么输出的结果应该是从1到10
[2, 2, 3]
[1, 2, 3]
[4, 2, 3]
[5, 2, 3]
[3, 2, 3]
[6, 2, 3]
[9, 2, 3]
[10, 2, 3]
[8, 2, 3]
[7, 2, 3]
输出结果可以证明channel传输的每个对象都是copy的。如果我们启动了很多task,同时我们的数据又非常大,那么为每个task都copy副本会使我们的内存使用量膨胀而没有任何实际好处。
你可能注意到了:我们并没有为每一个线程创建一个channel,tx对象和rx对象都只有一个,为什么可以给多线程使用。我们使用了tx.clone()获得了tx的引用。(当我们需要一些类型可以让我们拥有一个值的多个有所有权的引用。我们使用Rc<T>
,它是一个引用计数类型用以提供共享的所有权。它有一些运行时记录来跟踪引用它的数量,也就是“引用计数”。调用Rc<T>
的clone()
方法会返回一个有所有权的引用并增加其内部引用计数,但Rc并不能在线程间安全的传递。这是因为其内部的引用计数并不是通过一个线程安全的方式维护的(非原子性操作)并可能产生数据竞争。)
为了解决这个问题,我们使用Arc<T>
,Rust 标准的原子引用计数类型。
Arc是“原子引用计数”的缩写,它是一种在多个task之间共享不可变数据的方法。我们也可以像tx对象那样将上面的代码改进为引用计数的形式:
use std::thread;
use std::sync::Arc;
use std::sync::mpsc; fn main() { let numbers = [,,];
let numbers_arc = Arc::new(numbers); let (tx, rx) = mpsc::channel();
for i in .. {
let tx = tx.clone();
// Create a new owned reference:
let numbers_arc = numbers_arc.clone();
// Use it in a thread:
thread::spawn(move || {
tx.send(numbers_arc).unwrap();
});
} for _ in .. {
let local_arc = rx.recv().unwrap();
println!("{:?}", local_arc);
}
}
这时Rust就不会copy对象了。这与我们之前的代码非常相似,除了现在我们循环10次,启动10个task,并在它们之间发送一个Arc。 Arc :: new创建一个新的Arc,.clone()返回Arc的新的引用。 因此,我们为每个task创建一个新的引用,将该引用发送到通道,然后使用引用打印出一个数字。 现在我们不copy vector。
Arc非常适合不可变数据,但可变数据呢? 共享变量是并发程序的祸根,其实我们应该使用线程间通讯,能摒弃的都摒弃,如果你的变量是简单竞争的,不想使用通讯,才考虑使用锁。
您可以使用互斥锁(Mutex)来保护共享的可变状态,代码这样修改:
use std::thread;
use std::sync::{Arc, Mutex};
use std::sync::mpsc; fn main() { let numbers = [,,];
let numbers_arc = Arc::new(Mutex::new(numbers)); let (tx, rx) = mpsc::channel();
for i in .. {
// Create a new owned reference:
let (data, tx) = (numbers_arc.clone(), tx.clone());
// Use it in a thread:
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[] += i;
tx.send(*data).unwrap();
});
} for _ in .. {
let local_arc = rx.recv().unwrap();
println!("{:?}", local_arc);
}
}
互斥锁太简单粗暴了,Rust为共享可变状态还提供了一个工具:RwLock。读写锁,它的特点是:同时允许多个读,最多只能有一个写;读和写不能同时存在;
RwLock的API与Mutex略有不同:有read和write方法允许您读取和写入数据。 这两个方法都将闭包作为参数,并且在写入的情况下,RwLock将获取互斥锁,然后将数据传递给此闭包。 闭包完成后,互斥锁被释放。
你可以看到Rust在你不记得获取锁的情况下是不可能让你改变共享变量的。 我们获得了共享可变状态的便利,同时保持不允许共享可变状态的安全性。
unsafe
虽然Rust编译器非常聪明,并且可以避免你通常犯的错误,但它不是人工智能,我们比编译器更聪明,在某些代码场景中,我们需要打破这种安全检查。 为此,Rust有一个unsafe关键字。 在一个unsafe的代码块里,Rust关闭了许多安全检查。 如果您的程序出现问题,您只需要审核您在不安全范围内所做的事情,而不是整个程序。
如果Rust的主要目标之一是安全,为什么要关闭安全? 嗯,实际上只有三个主要原因:与外部代码连接,例如将FFI写入C库;性能,(在某些情况下)围绕通常不安全的操作提供安全抽象。 我们的Arcs就是后一个目的的而使用unsafe关键字。
我们可以安全地分发对Arc的多个引用,因为我们确信数据是不可变的,因此可以安全地共享。 我们可以分发对RWArc的多个引用,因为我们知道我们已经将数据包装在互斥锁中,因此可以安全地共享。
但Rust编译器无法知道我们已经做出了这些处理,所以在Arc/RWArc的实现中,我们使用unsafe的块来做了(通常来说)一些危险的事情。但是我们暴露了一个安全的接口,这意味着Arc/RWArc不可能被错误地使用。
这就是Rust的类型系统如何让你不会犯一些使并发编程变得不可控的错误,同时也能获得像C++等语言一样的效率。
一文学会Rust?的更多相关文章
- 一文学会JVM性能优化
实战性能优化 1 重新认知JVM 之前我们画过一张图,是从Class文件到类装载器,再到运行时数据区的过程,现在咱们把这张图不妨丰富完善一下,展示了JVM的大体物理结构图. 执行引擎:用于执行JVM字 ...
- 一文学会 TypeScript 的 82% 常用知识点(下)
一文学会 TypeScript 的 82% 常用知识点(下) 前端专栏 2019-11-23 18:39:08 都已经 9021 年了,TypeScript(以下简称 TS)作为前端工程师不得 ...
- 一文学会text-justify,orientation,combine文本属性
大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...
- 一文学会MySQL的explain工具
开篇说明 (1) 本文将细致介绍MySQL的explain工具,是下一篇<一文读懂MySQL的索引机制及查询优化>的准备篇. (2) 本文主要基于MySQL5.7版本(https://de ...
- 【Flutter 实战】一文学会20多个动画组件
老孟导读:此篇文章是 Flutter 动画系列文章第三篇,后续还有动画序列.过度动画.转场动画.自定义动画等. Flutter 系统提供了20多个动画组件,只要你把前面[动画核心](文末有链接)的文章 ...
- 一文学会Java的交互式编程环境jshell
什么是交互式编程环境?重点词交互,在这样的编程环境中,你每输入一行代码,环境都会给你一个反馈,这就是交互式的编程环境.这种编程环境并不太适合工程化的复杂性需求,但在一些快速验证.简单计算之类的场景下还 ...
- 文盘Rust -- struct 中的生命周期
最近在用rust 写一个redis的数据校验工具.redis-rs中具备 redis::ConnectionLike trait,借助它可以较好的来抽象校验过程.在开发中,不免要定义struct 中的 ...
- 文盘Rust -- 把程序作为守护进程启动
当我们写完一个服务端程序,需要上线部署的时候,或多或少都会和操作系统的守护进程打交道,毕竟谁也不希望shell关闭既停服.今天我们就来聊聊这个事儿. 最早大家部署应用的通常操作是 "nohu ...
- 一文学会Scala
整体介绍 Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性. 联邦理工学院洛桑(EPFL)的Martin Odersky于2001 ...
随机推荐
- httprunner如何提取数据串联上下游接口
httprunner进行接口测试时,从上一个接口提取参数传递给下游接口,如何获取数据里最后一个值? 突然被学员问道一个httprunner的问题,惭愧的是大猫之前没有是通过httprunner,又不好 ...
- 新建ext4分区及开机挂载
1.查看新的20G硬盘是否已经挂在完毕. 2.使用fdisk命令创建主分区 3.再将分区设置完毕之后,查看磁盘分区是否创建完成. 2.使用mkfs.ext4命令将新创建的分区进行格式化为: 1)blo ...
- java 集合之HashMap、Hashtable、LinkedHashMap、TreeMap
HashMap 实现了Map接口,线程不安全. 实现原理: HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的. 如果通过hash定位到数组位置没有链表, ...
- 使用XML的方式实现账户的CRUD
1 需求和技术要求 1.1 需求 实现账户的CRUD. 1.2 技术要求 使用Spring的IOC实现对象的管理. 使用QueryRunner作为持久层的解决方案. 使用C3p0作为数据源. 2 环境 ...
- Python 3标准库第三章
时间过得很快,又是一周过去了,今天感觉迷迷糊糊的,不在状态,然后,下面开始我们的讲解,还是跟大家分享一下我自己的一些想法,第一.怎么讲了,就是各位如果有怀才不遇的想法,我感觉最好不要有这种想法;第二. ...
- 求hack or 证明(【JZOJ 4923】 【NOIP2017提高组模拟12.17】巧克力狂欢)
前言 本人在此题有一种不是题解的方法,但无法证明也找不到反例. 如果各位大神有反例或证明请发至 邮箱:qq1350742779@163.com Description Alice和Bob有一棵树(无根 ...
- maven 三个仓库表
https://search.maven.org ,http://www.mvnrepository.com/ http://maven.apache.org
- Codeforces 912E Prime Gift ( 二分 && 折半枚举 && 双指针技巧)
题意 : 给你 N ( 1 ≤ N ≤ 16 ) 个质数,然后问你由这些质数作为因子的数 ( 此数不超 10^18 ) & ( 不一定需要其因子包含所给的所有质数 ) 的第 k 个是什么 分析 ...
- Codeforces 919D Substring ( 拓扑排序 && DAG上的DP )
题意 : 给出含有 N 个点 M 条边的图(可能不连通或者包含环),每个点都标有一个小写字母编号,然后问你有没有一条路径使得路径上重复字母个数最多的次数是多少次,例如图上有条路径的顶点标号顺序是 a ...
- css使用2
一.盒子模型 盒子模型 margin:用来调节盒子与盒子之间的距离(标签与标签之间距离) border:盒子的包装厚度(边框) padding:内部物体与盒子之间距离(文本与边框之间的距离) cont ...