Rust借用机制的理解分析

Rust初学者大多会遇到这样的问题:

  • 为什么同一资源不可被同时可变借用和不可变借用?
  • 为什么Rc一定只能是只读的,一定要配合std::cell系列(Cell,RefCell,UnsafeCell)才能提供可变性?为什么不设计一个可变的Rc?
  • 为什么Mutex/RwLock一般都会配合Arc使用?
    ……

这一类借用规则的问题,实际上都可以通过另一种思路来很好地解答,当然这也是很多大佬一定程度上认可的理解方式:

Rust中的借用,分为不可变借用共享借用,和可变借用独占借用
无论在教程中,还是语法中,我们都用可变性来区分二种借用,最开始,最直观的理解:

  • 因为是可变借用,多方借用会导致冲突,因此可变借用是独占的。
  • 因为是不可变借用,多方借用只读数据不会导致冲突,因此不可变借用是可共享的。

但是我们可以试着换一个方向去思考,用独占性去区分两种借用,很多问题便可以轻易理解:

  • 因为是独占借用,对资源的任何形式的访问都只能有一方可以被允许,所以对它的改变是安全无害的。
  • 因为是共享借用,对资源的访问可以有多方共同进行,所以对其中任何一方而言,来自其他方的改变是不可控的,所以对它的改变并不是安全无害的,因此它只允许只读访问。

这样的思考方式从“可变性==>独占性”换成了“独占性==>可变性”,首先就直观地解释了“同一资源不可同时被可变借用和不可变借用”的问题,因为“独占借用”从字面上就不可能跟其他任何形式的借用共存。

再说到Rc/Arc。Rc是通过引用计数来提供安全的共享访问的只能指针,在C++中本就叫做"shared_ptr",从逻辑上就是提供共享访问的工具,不应被任何一方独占,因此也理应只提供共享借用。而Arc就是线程安全版的Rc。
无论是std::cell系列还是Mutex/RwLock,它们的目的都一致:为共享资源提供安全的可变访问。由于既需要共享,也需要可变,因此无法用“独占=可变,共享=不可变”的类型模型去编译期检查访问的安全性,只能在运行时通过实际的行为去动态检查,保证“同一时间,一个资源只可以被一方可变访问,或被多方不可变访问”的原则。只是它们的行为略有不同:

  • UnsafeCell仅仅是一层包装壳,在对本身的访问不需要所有权或独占借用,仅提供共享借用的前提下,提供对内含资源的独占引用访问,因为没有任何策略保证访问的安全,因此对其的操作是unsafe的。
  • Cell屏蔽掉了UnsafeCell的引用访问,只提供存取访问。由于在单线程下,调用函数即完全交出控制权,存取都可被视为原子操作,而资源本身也从一个静态的资源变成了一个需要操作去存取的读写对象,因此自然也保证了安全访问的原则。
  • RefCell在UnsafeCell的基础上,以性能为代价,加入了运行时检查机制以提供安全的引用访问。由于引用可以在调用结束后持续地被持有,因此需要guard(通过自身的RAII来控制目标资源的占用与解除占用的结构)来运行时监控当前资源被访问的形式和数量,在打破安全访问的原则时及时抛出panic来避免ub。
  • RwLock是线程安全版的RefCell,通过调用系统底层的线程锁来实现线程安全,在打破安全访问原则时阻塞后续的访问而不是panic,以等待目前的占用解除。
  • Mutex是只读访问互斥版的RwLock,即使不存在可写访问,仍然会在已有只读访问占用时阻塞后续的只读访问。

不难看出,很多情况下,Arc<RwLock>的组合实际上跟Rc<RefCell>的逻辑是一致的:在需要共享的可变资源时,先用Rc/Arc控制资源的生命周期,再用RefCell/RwLock来控制资源的访问安全,区别在于是否为跨线程访问提供支持。

综上所述,以个人经验来讲,在习惯上以独占借用/共享借用的视角来分析,而不是可变借用/不可变借用,更有助于理解借用规则的实质,希望以此可以给初接触Rust的同行一些启发。

posted @ 2020-10-22 10:42  Alsein  阅读(292)  评论(0编辑  收藏  举报