介绍

本系列录制的视频主要放在B站上​​Rust死灵书学习视频​

Rust 死灵书相关的源码资料在https://github.com/anonymousGiga/Rustonomicon-Source

原子操作

Rust的原子操作模型基本和c11的原子操作模型一样。

编译器重排

编译器努力地通过各种复杂的变换,尽可能减少数据依赖和消除死代码。特别是,它可能会彻底改变事件的顺序,或者干脆让某些事件永远不会发生。

x = 1;
y = 3;
x = 2;

编译器可能的优化:

x = 2;
y = 3;

硬件重排

麻烦来自于在内存分层模式下的 CPU。你的硬件系统里确实有一些全局共享的内存空间,但是在各个 CPU 核心看来,这些内存都离得太远,速度也太慢。CPU 希望能在它的本地 cache 里操作数据,只有在 cache 里没有需要的内存时才委屈地和共享内存打交道.

cache里的内容需要更新,结果就是硬件不能保证相同的事件在两个不同的线程里一定有相同的执行顺序。

考虑代码:

初始状态: x = 0, y = 1

线程1 线程2
y = 3; if x == 1 {
x = 1; y *= 2;
}

可能出现几种情况:

  • y = 3:线程 2 在线程 1 完成之前检查了 x 的值
  • y = 6:线程 2 在线程 1 完成之后检查了 x 的值
  • y = 2:线程 2 看到了 x = 1,但是没看到 y = 3,接下来用计算结果覆盖了 y = 3(硬件重排可能创造出来的结果)。

一般硬件排序分两类:强顺序和弱顺序。

  • 在强顺序硬件上要求强顺序保证的开销很小,甚至可能为零,因为硬件本身已经无条件提供了强保证。而弱保证可能只能在弱顺序硬件上获得性能优势;
  • 在强顺序硬件上要求过于弱的顺序保证有可能也会碰巧成功,即使你的程序是错误的。

数据访问

程序的正确执行必须要有时间吸纳后的关系,我们通过“数据访问”和“原子访问”来控制这种关系。

数据访问是程序设计的基础,它们是非同步的,编译器优化时,可能认定数据访问都是单线程的,可以对它进行任意重排。

只依靠数据访问是不可能写出正确的同步代码的。

原子访问

原子访问告诉硬件和编译器,程序是多线程的,每种原子访问都关联一种排序方式,以确定它和其它访问之间的关系。

Rust暴露的排序方式包括:

  • 顺序一致性(SeqCst);
  • 释放(Release);
  • 获取(Acquire);
  • 松散(Releaxed)。

顺序一致性

  • 顺序一致性操作不能被重排,同一个线程中,SeqCst之前的访问永远在它之前,之后的访问永远在它之后;
  • 只使用顺序一致性原子操作和数据访问可以构建一个无数据竞争的程序。
  • 顺序一致性不是免费的,即使在顺序平台上,顺序一致性也会产生内存屏障。
  • 顺序一致性很少是程序正确性的必要条件。

获取-释放

  • 获取和释放经常成对出现。它们适用于获取和释放锁,确保临界区不会重叠。
  • acquire保证在它之后的访问永远在它之后,但是在它之前的操作也有可能被重排在它之后。
  • 强顺序平台上,大多数访问都有释放和获取语义,通常是无开销的。

松散

  • Relaxed访问最弱,可以被随意重排,没有先后关系。
  • 在强顺序平台上,使用Relaxed没有什么好处,不过在弱顺序平台上,Relaxed可以获取的开销最小。