基本类型存在栈上,复制成本非常低。但实际编程中一定会需要复杂结构体,复杂数据存储于某个对象。对于复制这一定义,对应 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。
编译器会做代码优化,写了3步的操作,会给你优化到一步完成。cpu会分支预测,乱序执行,猜测下一步做什么,提前做,只要最后的结果是正确的,代码执行顺序不重要。这些都是在单线程的时候,全是好处的优化。
当多线程的时候,会不可避免的遇到数据的共享与共享数据的互操作问题。当代码执行与逻辑判断的相关变量存在另一个线程上时,你需要的变量,状态变化不是预想的顺序,你针对这个数据的预想处理逻辑根本就无法适配了。
不变的数据符合send吗:首先,一种不可变的数据是完美符合的,不变的数据只有两种状态,未创建与创建之后,创建之后在无数线程中互相传递也没问题,应为数据不变,每个线程里看到的数据都是一样的。但是当我们创建了无数不变的数据,在线程之中互相传递时,其实早就预设了一种立场,即这些数据是长久存在的。在有gc的编程语言里,当一个数据不被使用后会有复杂的机制,去查看这个数据是否无用了,然后无用数据回收。但是查看的操作是很耗资源的。在rust这种无gc的数据,如果你预设了数据在多线程里长久存在,那么在最后一个使用数据的线程中,怎样才能知晓这是该数据的生命终点呢?如果你在多线程中传递的数据,每一份都是独立的,每一份都是clone出来的,在每个线程结束后销毁自己的数据,这个性能怕是比有gc语言性能更差。所有rust的逻辑是绝大多数变量就地释放,一个不可变数据从a线程传到b线程,对应a来说,就算释放了,数据的真正释放由b线程负责,当你还想在a线程将这份数据再传给c线程是不可以的,除非你提前clone了一份数据。
现实中我们应该尽量避免深拷贝(clone),所以正常情况下,想要线程之间随意使用数据,且不过度消耗资源,最明显的答案是什么?是只保留一份不可变的数据,且保证其寿命足够长,且在最后不被使用了是应该回收的。rust是实时回收的,所以普通变量无法保证其寿命在多线程中足够长。但rust本身提供了满足这种条件的解决方案,那就是Arc,它的能力是指向(包裹)一份“不变的”数据,向外提供多个引用,以便外面多个线程读取数据。每向外提供一份引用,Arc内部有一个计数器就加一,如果某个线程结束了,那就drop了这个引用,Arc内部的计数器减一。在计数器归零前,任何一个线程都不负责数据的销毁,那些线程销毁的只是引用以及让计数器减一。知道Arc计数器归零了,其包裹的数据才到了销毁的时候。
所谓send,rust中绝大多数类型都是满足的,send其含义就是,可以从a线程发送到b线程,其行为有点类似move。几乎没有不满足的理由。少数的反例,比如Rc(引用计数)类型,它不独立于任意线程,每多一个/少一个它的引用,计数会变,当多个线程同时使用时,其内部计数器无法保证同步,即计数器会有“量子叠加态”,所以它不是send。(不知道这样解释对不对。。) 当数据满足了send之后,其在线程中可以随意移动,更具体说,是在任意两个线程中移动,这个移动是一次性的,移过去后,原线程就再也没有了。
那么所谓在任意线程中随意移动也是预设了什么能力,这个能力就是sync,即要同时满足send和sync,才可以随意同步。之前预设的这个sync能力就是在多个线程中同时存在。
最后来解释一下,大多数时候在rust中的多线程编程中,我们希望数据可以在多个线程中任意传递。其本质是,只有一份数据,首先能从a线程传到b线程,即满足send;其次每个线程留有一个入口(类似于引用),可以通过入口重新接触到数据,进行读取或修改,多个线程,能有多个入口,即sync。实质上,就是一份数据在不同线程间流转,每个线程可以在需要时通过入口再次接触数据。增加send和sync之后,好像不知道有什么用处,本质上是避免了“量子叠加态”,满足这两个trait,那么不会有这方面的竞态条件。
Arc
Mutex
最后,逻辑上的漏洞是难以避免的。