aehrfbszer

rust 基本概念的一些个人解释

基本类型存在栈上,复制成本非常低。但实际编程中一定会需要复杂结构体,复杂数据存储于某个对象。对于复制这一定义,对应 Clone 这个trait。所有想能够复制的数据结构,都要实现这个Clone。然后,对于复制成本极低的数据结构(并不一定要基本数据类型),定义了 Copy 这个trait,要实现copy需要先实现clone,在手动调用复制的行为(调用clone方法)上,两种无差别,copy 这个trait里面没有内容,调用的就是clone的方法。注:具体的底层实现不一定这样,我只是表述我的逻辑。

clone于copy的区别在于,copy这个trait无法被手动调用,只要在一个变量名称对应的数据,变换到另一个变量名称时,自动调用了copy行为,如果这个数据类型未实现copy trait,那么就是进行了move行为。

move行为发生后,一个数据的新旧两个变量名,旧的失去了存在的意义,对旧的进行任何有意义操作,都是报错。新变量名是数据的新所有者。在copy行为下,新旧二者同时存在,是两个独立的存在,两份独立数据,互不干扰,各自可独立修改。

但正常的逻辑,对于某一个数据源,都是会进行多次操作,可能是查看数据然后继续分支逻辑,可能是查看并修改数据。反正查看是非常频繁的事。如果数据只能move,那么应该是写不了正常的代码逻辑了。然后,就有了引用(指针)的概念,指针是一个普通的基本类型 usize ,根据操作系统不同,有不同大小,例如64位系统对于u64。所谓指针(引用),就是对应数据源的地址编号。

一般来说,copy/move行为发生,那就是统一认为在另一个内存地址得到了一份新数据。(实际上copy是这样的;变量名其实就是指针,move其实是数据地址不变,搞了一个新的变量名/指针)这样认为是为了减少心智负担,rust没有gc,确保数据源在被使用的上下文中切实存在,所有的引用可以切实在 上下文/周围 找到数据源。如果不做这种假设,move了几十次,存在几十个来源不同变量名的引用,虽然对应同一个地址的数据源,但非人类可读,可能几百行代码,已经分不清来源是谁了。

为了确保在无gc的情况下,能在附件上下文找到来源,就有生命周期的概念。如果是一个独立完整不依赖其他数据的数据源,它不需要生命周期,生命周期就是它生到它死。现在一个引用指向某个数据源,为了能切实找到对应数据,那么引用的生命长度是:引用产生到引用废弃/数据源死亡。引用产生一定在数据源产生之后,也必须消亡于数据源消失之前或同一时刻。(但是引用可以单独作为一个可变的存在usize,可以指向不同数据源,所以不一定绑死一个数据源的生命周期,但这只是可变mut的情况,rust默认不可变)

现在是多线程的情况:几乎所有基本数据都可以在线程之间移动。所谓多线程,和cpu的能力有关,现代多核cpu,能做通用计算,但每个单独cpu都是相对独立的,每个cpu都有处理能力,也都有自己的数据/缓存。没有绝对的主次,最早的线程就是一个cpu对应一个线程。普通程序都是运行在一个线程/主线程上,到了多线程代码部分,由于数据是相对隔离的,并且线程之间也是消亡时间不等的,有的早消亡,有的晚。直接将 数据源/所有权 抛给另一个线程,这叫做 send,对于原线程,抛出去的数据源,生命周期就结束了。

我们往往不需要复制,因为复杂数据的复制是耗时/浪费资源的,所以某个变量(不管是数据源还是引用)的引用在不同线程中共享(读取数据/同步数据)的能力,就叫sync。某个变量是sync,就是该变量的引用(usize)是可复制且可传播到不同线程的。 所以存在某个 T ,其引用 &T 是send,那么 T 是 sync。

rust是无gc的,所谓无gc且不像C++一样需要手动释放,那么就意味着rust是实时gc的,即绝大多数变量在超出使用范围后被销毁(在使用范围末端drop了)。要记住绝大多数变量是实时drop的!

编译器会做代码优化,写了3步的操作,会给你优化到一步完成。cpu会分支预测,乱序执行,猜测下一步做什么,提前做,只要最后的结果是正确的,代码执行顺序不重要。这些都是在单线程的时候,全是好处的优化。

当多线程的时候,会不可避免的遇到数据的共享与共享数据的互操作问题。当代码执行与逻辑判断的相关变量存在另一个线程上时,你需要的变量,状态变化不是预想的顺序,你针对这个数据的预想处理逻辑根本就无法适配了。

为了多线程的逻辑,当一种数据能在线程之间互相随意传递时,先把这种能力叫做send。到底什么算send呢?

澄清Arc与Mutex,什么是真正的send,以及缺了什么

上面的结论都是对的吗?其实不是很对,上面的讨论都有一个预设前提:那就是上面所谓的数据,本身就应该是send的。

所谓send,rust中绝大多数类型都是满足的,send其含义就是,可以从a线程发送到b线程,其行为有点类似move。几乎没有不满足的理由。少数的反例,比如Rc(引用计数)类型,它不独立于任意线程,每多一个/少一个它的引用,计数会变,当多个线程同时使用时,其内部计数器无法保证同步,即计数器会有“量子叠加态”,所以它不是send。(不知道这样解释对不对。。) 当数据满足了send之后,其在线程中可以随意移动,更具体说,是在任意两个线程中移动,这个移动是一次性的,移过去后,原线程就再也没有了。

那么所谓在任意线程中随意移动也是预设了什么能力,这个能力就是sync,即要同时满足send和sync,才可以随意同步。之前预设的这个sync能力就是在多个线程中同时存在。

最后来解释一下,大多数时候在rust中的多线程编程中,我们希望数据可以在多个线程中任意传递。其本质是,只有一份数据,首先能从a线程传到b线程,即满足send;其次每个线程留有一个入口(类似于引用),可以通过入口重新接触到数据,进行读取或修改,多个线程,能有多个入口,即sync。实质上,就是一份数据在不同线程间流转,每个线程可以在需要时通过入口再次接触数据。增加send和sync之后,好像不知道有什么用处,本质上是避免了“量子叠加态”,满足这两个trait,那么不会有这方面的竞态条件。

Arc will implement Send and Sync as long as the T implements Send and Sync.

Mutex will implement Sync as long as the T implements Send.

最后,逻辑上的漏洞是难以避免的。