陈天老师的Rust培训(1)学习笔记

news2024/12/29 19:38:22

https://tyrchen.github.io/rust-training/

 

 

 

 

 

 

 

跟C互操作时,加上上面图片的宏,rust会根据你的各个域的内存的占用自动去调整内存中的位置,让你的内存占用是最优的,而且rust在生成数据结构的结构的时候,它会做一些padding,然后这些padding是为了具有更好的performance,但是这样就会对结构体做一些顺序上的调整,以及加一些padding.

函数的话,如果你看rust生成的assembly code的话,所有的函数都被转换为一个奇奇怪怪的名字,所以在rust里面,如果想跟C互操作的话,对于函数需要加上#[no_mangle],不要去做函数的混淆,data struct 需要做一个repr(C)

 

 

 

使用unsafe还有个好处就是,当你的代码有内存泄露或者有concurrency issue的时候,你马上就知道从哪里去着手,去看问题的所在。相对于C、C++的项目出现这样一个问题,可能是大海捞针,得到处去找问题的根源,而这个问题的根源在大型C、C++项目里面是非常难找的。

有了unsafe之后,如果出现上述问题,肯定就是unsafe代码里面。

 

 

函数A call 函数B ,会给函数B生成一个新的frame,然后把一些信息填进去,比说函数A的return Address放进去,然后把这些栈帧都保留进去,然后给函数B传的参数push进去,因为编译器在编译期就知道函数B都用了哪些局部变量,这些局部变量的大小是什么,然后为这些局部变量reserve好这个栈。

一个frame一个frame去压栈。最后return的时候,一个frame一个frame的返回。

 

 

 

 

 

 

上图是单线程里面。总是可以的

但是如果是多线程,如下,就可能有问题了

 T1 call一个函数,这个函数生成一个新的thread,然后这个thread把ptr move到新的thread下面。当这种情况发生的时候,compiler就没办法区别了。lifetime没有单线程的那么清晰,因为T1随时可能会结束,T1结束的时候,那么T2里面的ptr指向T1之前的那个有效内存,这个时候栈已经走了,栈走了再引用栈上的数据是非常危险的,因为已经不知道数据时什么样子的了。

 

Sync是把reference在多个线程之间传递,Send是支持值在多个线程间传递,然后它还加了个'static, 'static是 lifetime bound,也就是说这个F闭包里面不能使用任何借用的数据,只能使用你own的数据。

回到上上上个图,inser(&user)这是借用一个数据,上上个图T1把ptr move到T2 ,但是这是一个引用,不是一个own的data,收益在上图spawn的signature下面,编译器是不会让你编译通过的 ,所以就规避了有问题引用的发生。

 

Rust可以借用栈和堆上的内存,这比其他很多语言要灵活,在写C的时候,一个大忌就是不能引用栈上的内存,引来引去之后就会出错,但rust用了一套很清晰的规则,然后让你可以很安全的借用栈上的内存,借用栈上的内存效率是非常高的,栈上的内存分配和释放都是非常高效的、

但是正因为很多语言没有设置这些限制,就导致最终不得退而求其次去借用堆上的内存,比如java你去引用 只能引用堆上的内存。

 

pub fn strtok(s: &mut &str, pattern: char) -> &str {
    match s.find(pattern) {
        Some(i) => {
            let prefix = &s[..i];
            let suffix = &s[i + pattern.len_utf8()..];
            *s = suffix;
            prefix
        }
        None => {
            let prefix = *s;
            *s = "";
            prefix
        }
    }
}

 

pub fn strtok<'a>(s: &'a mut &str, pattern: char) -> &'a str {
    match s.find(pattern) {
        Some(i) => {
            let prefix = &s[..i];
            let suffix = &s[i + pattern.len_utf8()..];
            *s = suffix;
            prefix
        }
        None => {
            let prefix = *s;
            *s = "";
            prefix
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let mut s = "hello world";
        assert_eq!(s.find(' '), Some(5));
        // let mut s1 = &mut s;这里不是mut本身,而是mut s1指向的str,所以不需要mut
        let s1 = &mut s;
        let t = strtok(s1, ' ');
        assert_eq!(t, "hello");
        assert_eq!(*s1, "world");
        assert_eq!(s, "world");
    }
}

 

bss section

data secton

text section 就是我们常说的代码段

闭包不一定是static lifetime的

string literals是会被编译到binary里面的string section

编译好,之后内存地址固定。

 

 ticket.rs

 

 

传值的时候一定是在新生成的栈里面把你要传的数据拷过去,任何语言都是这么去做的。

很多时候传值并不是最大的性能杀手,最大的性能杀手其实是堆上的性能分配,因为往往你在堆上分配内存,你需要往里面拷贝东西,回头你还得释放这块内存,这里就比单纯栈上拷贝花费的更大,而堆上内存的分配,往往又涉及到调用这个操作系统的malloc。

内存布局

 Rust Language Cheat Sheet

 

call 这个代码的时候(let writer:&mut dyn Write = &mut buf;),rust会自动生成trait object,

结构就是

 

ptr本身指向原来的vec,然后vec自己的ptr/cap/len是分配在栈上的, 具体的数据是ptr指向的heap上的数据。

meta可以理解为vptr,指向vtable,一个virtual table,因为当我生成一个trait object 的时候,原有的type就被抹掉了,再次使用writer的时候,并不知道writer原来的type是什么。原来它的type是个vec,但是在变成一个trait object的时候,已经没有这个信息了,然后我有的信息就是它实现了write这个trait,所以它有所有的trait method,另外,所有datastructure应该都有实现的drop trait,另外还有size alignment等其他信息。

trait object的好处它是一个在runtime生成的这么一个类型,这个类型不存具体的数据结构,那就使得我们很方便的进行动态的分发。

pub trait Formatter {
    fn format(&self, input: &mut str) -> bool;
}

struct MarkdownFormatter;

impl Formatter for MarkdownFormatter {
    fn format(&self, input: &mut str) -> bool {
        todo!()
    }
}

struct RustFormatter;

impl Formatter for RustFormatter {
    fn format(&self, input: &mut str) -> bool {
        todo!()
    }
}

struct HtmlFormatter;

impl Formatter for HtmlFormatter {
    fn format(&self, input: &mut str) -> bool {
        todo!()
    }
}

pub fn format(input: &mut str, formatters: Vec<Box<dyn Formatter>>) {
    for formatter in formatters {
        formatter.format(input);
    }
}

上述代码,为什么Vec里面直接包含上述几种类型呢?我们知道在rust下面,一个vec里面包含的数据都是同一种类型,没有任何一种datastructure是Iterable的,它里面的数据类型又是不一样的,唯有把它转成trait object,然后所有类型都统一了,然后就可以去使用,这是rust的动态分发。

 上述代码,为什么Vec里面直接包含上述几种类型呢?我们知道在rust下面,一个vec里面包含的数据都是同一种类型,没有任何一种datastructure是Iterable的,它里面的数据类型又是不一样的,唯有把它转成trait object,然后所有类型都统一了,然后就可以去使用,这是rust的动态分发。

 

 

 

use anyhow::Result;

pub trait Encoder {
    fn encode(&self) -> Result<Vec<u8>>;
}

pub struct Event<Id, Data> {
    id: Id,
    data: Data,
}

impl<Id, Data> Event<Id, Data>
where
    Id: Encoder,
    Data: Encoder,
{
    pub fn new(id: Id, data: Data) -> Self {
        Self { id, data }
    }
    pub fn do_encode(&self) -> Result<Vec<u8>> {
        let mut result = self.id.encode()?;
        result.append(&mut self.data.encode()?);
        Ok(result)
    }
}

impl Encoder for u64 {
    fn encode(&self) -> Result<Vec<u8>> {
        Ok(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]) //测试使用
    }
}

impl Encoder for String {
    fn encode(&self) -> Result<Vec<u8>> {
        Ok(self.as_bytes().to_vec())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let event = Event::new(1, "hello world".to_string());
        let _vec = event.do_encode().unwrap();
    }
}

rust在实现泛型的时候跟C++很类似,它会为每种使用到的类型都生成一个版本,这也是rust在编译代码的时候比较慢,

let event = Event::new(1, "hello world".to_string());
let _vec = event.do_encode().unwrap();

let event = Event::new("hello world2".to_string(), "hello world".to_string());
let _vec = event.do_encode().unwrap();

上述代码Event就会被编译成两个版本,它的所有函数也会被编程两个版本,来处理不同的类型。这就是支持泛型的语言,一般编译都比较慢,这也是为什么go迟迟不愿意加入泛型,又不影响它引以为豪的编译速度。一旦引入泛型,编译的速度一定会打折扣,这种你生成多个版本,还有一个潜在问题就是,你在release你的binary的时候,你的binary很多被当作一个商业的版本去用,因为一旦编译好,这个组件所有的泛型里面用到的具体的类型会被编译出来,泛型本身不会存在编译的binary里面。所以这东西提交给另外一个公司,公司依赖着你的binary去做coding,它是非常受限的。

到目前为止,像rust,如果要引入别人的库 ,只能通过源码来引入,你使用了泛型的,如果你不用源码的话,基本上是没办法用的。

但是swift就才用了不同的方式,生成一个xxtable。具体的可以自己查看下。

 

 

 

 上图,把store shared成N份,这样锁的粒度变小

 上图是golang推崇的

 

上图 async task适用于IO 密集的任务,所以socket处理数据的收发,适合async task

但是你对内存上面的这个data用async data 没多大意义。

 

 

 

 

 

 

 

 

 上图是因为compare_exchange开销非常大,可能设计到硬件,可能还要锁总线。所以我们做一次之后,发现它返回error,我们不要一直在这句话循环,我们尝试去load这个lock。因为不需要做swap的操作,只是看当前的状态是不是等于true,如果是true,就可以先把thread 挂起,如果等于false,再回到compare_exchange去循环,这样效率会更高的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 它是有一个很轻量的用户态的线程,它有很小的stack,然后在用户态run一堆的scheduler

 

 

 

 

 rust的设置应该受到了JavaScript上面设计的影响,引入状态机。所有语言的sync await 大概类似上面的东西,就是当你把一个异步的操作,在写代码的时候,你的感知是一个同步的操作,才用的方式就是,把这些东西转换成一个个状态机,去处理状态机的变化。

 

rust的处理是非常优美的async实现,预计未来其他的语言,尤其是新生代的语言,肯定是靠近rust这种实现方法。

从语法的层面就是async和await

从数据类型层面它是有Future,Poll,Waker

首先看 async function ,用async来修饰一个正常的function,这是一个语法糖,它内部实际上是一个返回值为impl Future的方法,这里是u8

 

 

一般自引用的类型通过pin来保证结构的类型安全。

poll里面有两个:

1、self: rust的async/await也是在pin这东西标准化后,也就是找到这么个解决办法后,async、await最终才标准化。

2、还有一个就是context,可以认为context里面含有一个waker,这个waker知道是如何把这个future再度唤醒。

然后基本的逻辑就是,这个poll function,rust future是需要不断的poll的,每次call这个poll function,要么取得进展,要么就结束了 这个future就退出了,要么future就处在这个pending的状态,它就返回,被返回到队列里面,等待下次被唤醒又继续能执行。

一个async的function,它的内部其实是有一套复杂的状态机来处理的,基本上每一个await代码的使用,你都可以认为生成一个新的状态

 

上图红色框,这个enum是非常节省内存的,不管多少个状态,这个enum的大小就是里面最大的那个。

ps:上图左边,是陈天老师,自己为了方便我们简单理解画的。

 

如上图,一开始的state是init状态,之后很快就切换到AwaitingStep1,当在这个状态的时候,它会去call step1这个future的poll,在poll里面,要么是ready,ready的话就可以把状态机切到AwaitngStep2,如果是pendding的话,就return,然后上图的整个future就先挂起,然后等待下次被唤醒,后面依次循环上述步骤,直到最后做完所有,就return ready

这个虽然内部生成的代码非常冗长,但是这个很冗长的代码,它的效率是非常高的,就是至少你手写的优化过的代码效率不会比这个高 这就是所谓的零成本,如果不用就不会生成,没有额外的cost,使用的话,生成的代码效率也是非常高。

rust的future是怎么run的呢?

分成了reactor和executor,reactor负责把这个future唤醒,executor负责具体执行这个future。

比如说socket,你有一个socket receive 的future,

现在有新的数据来了,

receiver就拿到了,

kernel就通知你这个data好了,

那reactor就把对应的future唤醒,

然后添加到run queue里面。

然后executor就从run queue里面pull出来,然后执行,要么执行完成,这个future就丢弃了,要么是pending,那么这个future就等待下一次被唤醒。

 tokio 使用的是work stealing scheduler,它在每个core都有一个scheduler,如果自己的local的run queue没东西了,它就会去别的地方偷,跟golang erlang的方式大同小异,

 

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

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

相关文章

新零售@2022: 调料、精酿、预制菜

【潮汐商业评论/原创】 圣诞节临近&#xff0c;刚刚下班的Emily想着好久没有与闺蜜见面了&#xff0c;于是相约着过平安夜。 她顺手就打开盒马APP下单了适合女生的微醺精酿&#xff0c;同时发现零售平台上新了一款可以用来拌蔬菜的中式沙拉川式椒麻汁&#xff0c;想起中午做饭…

常见音视频编码格式

1、常见的音频编码格式 MP3 这种压缩方式的全称叫MPEG Audio Layer3,MP3是利用MPEG Audio Layer 3的技术&#xff0c;将音乐以1:10甚至1:12 的压缩率&#xff0c;压缩成容量较小的file&#xff0c;换句话说&#xff0c;能够在音质丢失很小的情况下把文件压缩到更小的程度。而且…

收获一个python识别率超级高的OCR包,值得推荐

他的开源项目地址&#xff1a;https://github.com/sml2h3/ddddocr 他支持的类型还是挺多的&#xff1a; 本次更新新增了两种滑块识别算法&#xff0c;算法非深度神经网络实现&#xff0c;仅使用opencv和PIL完成。 算法1 小滑块为单独的png图片&#xff0c;背景是透明图&…

大一新生HTML期末作业——大学生抗疫感动专题网页设计作业

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

栈,队列和链表三者之间的关系与区别

最近一直在学习算法&#xff0c;刷算法题&#xff0c;但是自从大学毕业以来&#xff0c;数据结构的知识都还给老师了&#xff0c;只会个数组&#xff0c;所以前期刷的题目也都是有关数组的 最近跟着小册重学了一遍数据结构&#xff0c;今天就记录一下栈,队列和链表三者之间的关…

MindFusion.Diagramming for JavaScript V4.2.4

MindFusion.Diagramming for JavaScript V4.2.4 现在支持使用套索工具进行缩放的多种方式。2022 年 12 月 15 日 - 15:44新版本特征 套索缩放工具- 该控件现在支持使用套索工具进行缩放的多种方式。新的“PanAndModify”行为允许您在鼠标指针悬停在某个项目上或平移视图时进行选…

HTTPS工作过程!

HTTPS了解 HTTPS 也是一个应用层协议&#xff0c; 是在 HTTP 协议的基础上引入了一个加密层。HTTP 协议内容都是按照文本的方式明文传输的&#xff0c; 这就导致在传输过程中出现一些被篡改的情况。如运营商劫持。 HTTPS工作过程 ① 既然要保证数据安全, 就需要进行 “加密”。…

【python】如何把你的python包发布出去(pip install)

python&#xff1a;如何把你的python包发布出去&#xff08;pip install&#xff09; 介绍 实际上分为两步 打包发布 我们要发布的网站是https://pypi.org/。也就是用户通过pip install XXX&#xff0c;就可以安装你的包。 1 通过setuptools打包 需要我们编写setup.py f…

数据库的事务

作者&#xff1a;~小明学编程 文章专栏&#xff1a;MySQL 格言&#xff1a;目之所及皆为回忆&#xff0c;心之所想皆为过往 目录 什么是事务&#xff1f; 事务的特性 原子性 一致性 持久性 独立性 事务之间的影响 脏读 不可重复读 幻读 数据库的隔离级别 读未提交…

kvm介绍

kvm里主要去介绍它的虚拟化技术&#xff0c;包括云计算的组成和云计算的背景。 kvm的运行原理&#xff0c;虚拟机的创建&#xff0c;虚拟机的生命周期管理。 云计算的定义 它不是一种技术&#xff0c;它是一种收费模式&#xff0c;就是通过互联网把一些主机的硬件&#xff0…

2021年我国企业服务市场投融资概况 技术服务单笔规模最大 早期融资笔数最多

一、企业服务投融资年度概况 据相关数据显示&#xff0c;2021年我国企业服务市场共发生2417笔投融资事件&#xff0c;其中&#xff0c;1753笔项目已披露融资金额。从月度分布来看&#xff0c;3月、6月、7月、8月和12月的融资数量均在200起以上&#xff0c;其中&#xff0c;12月…

[附源码]Python计算机毕业设计高校学生体温管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Python实现:高斯滤波 均值滤波 中值滤波 Canny(边缘检测)PCA主成分分析 直方图规定化 Mean_Shift

Python实现&#xff1a;高斯滤波 均值滤波 中值滤波 Canny(边缘检测&#xff09;PCA主成分分析 直方图规定化 Mean_Shift&#xff08;文末附上整合这些函数的可视化界面并且已做打包处理&#xff09; 1.高斯滤波&#xff08;以下函数所有的图片路径为方便前来copy的同学&#…

微服务框架 SpringCloud微服务架构 多级缓存 46 JVM 进程缓存 46.4 实现进程缓存

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 多级缓存 文章目录微服务框架多级缓存46 JVM 进程缓存46.4 实现进程缓存46.4.1 实现进程缓存46 JVM 进程缓存 46.4 实现进程缓存 46.4.1 实…

Linux从入门到进阶学习(Ⅱ):Linux基础命令

目录 1 Linux目录结构 2 命令格式 3 目录切换命令 3.1 ls命令 3.2 选项与参数 3.3 目录切换 1.cd 2.pwd 4 路径 5 创建目录命令 6 文件操作命令 6.1 创建文件 6.2 查看文件 6.3 复制文件 6.4 移动文件 6.5 删除文件 1.rm命令 2.通配符 3.root用户 7 查找命…

C语言期末集训2(大一,超基础,小猫猫大课堂的配套练习)——分支结构

更新不易&#xff0c;麻烦多多点赞&#xff0c;欢迎你的提问&#xff0c;感谢你的转发&#xff0c; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我…

分库分表 15 连问,你抗的住吗?

我们去面试的时候&#xff0c;几乎都会被问到分库分表。 在这里整理了分库分表的15道经典面试题&#xff0c;大家看完肯定会有帮助的。 1. 我们为什么需要分库分表 在分库分表之前&#xff0c;就需要考虑为什么需要拆分。我们做一件事&#xff0c;肯定是有充分理由的。所以得…

技术分享-应用列表性能优化

阅读文章大约需要10分钟 目录 1.背景 2.分析 3.优化 4.成果 背景 应用存在大量的列表和图片资源加载&#xff0c;如首页、喵圈、直播间广播、礼物面板等, 这些列表的性能对应用性能有着不少的影响。 分析 分析-列表架构VLayout 分析-RecycleView缓存机制 分析-RecycleVi…

【记录】Ubuntu实现逻辑卷的删除

由于我的电脑上有机械硬盘和固态硬盘&#xff0c;所以在之前安装的Centos7系统中&#xff0c;定义了逻辑卷&#xff0c;希望将机械硬盘和固态硬盘在逻辑上当作是统一的整体&#xff0c;但是正因为此操作&#xff0c;导致在重装系统过程中&#xff0c;始终无法对逻辑卷进行分区&…

m基于改进PSO粒子群优化的RBF神经网络解耦控制算法matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 智能控制的思想最早来自傅京孙教授[&#xff0c;他通过人机控制器和机器人方面的研究&#xff0c;首先把人工智能的自觉推理方法用于学习控制系统&#xff0c;将智能控制概括为自动控制和人工智能…