Rust标准库中的同步介绍

Rust标准库中的同步介绍

https://doc.rust-lang.org/std/sync/index.html

有用的同步原语。

同步化的需要

从概念上讲,Rust程序是指在计算机上执行的一系列操作。程序中发生事件的时间线与代码中的操作顺序是一致的。

考虑一下下面的代码,在一些全局静态变量上进行操作。

static mut A: u32 = 0;
static mut B: u32 = 0;
static mut C: u32 = 0;

fn main() {
    unsafe {
        A = 3;
        B = 4;
        A = A + B;
        C = B;
        println!("{} {} {}", A, B, C);
        C = A;
    }
}

看起来好像是改变了一些存储在内存中的变量,进行了一次加法,结果存储在A中,变量C被修改了两次。

当只涉及到一个线程时,结果和预期的一样:7 4 4 4行被打印出来。

至于幕后会发生什么,当启用优化后,最终生成的机器代码可能会和代码中的样子大不相同。

  • 第一个存储到C的存储可能会被移到A或B之前,就像我们写的C = 4; A = 3; B = 4一样。

  • A+B的赋值可能会被删除,因为和可以存储在一个临时位置,直到它被打印出来,而全局变量永远不会被更新。

  • 最终的结果可以通过在编译时查看代码来确定,所以恒定折叠可能会把整个代码块变成一个简单的println!(“7 4 4 4”)。

编译器可以执行这些优化的任意组合,只要最终优化后的代码在执行时,产生的结果与没有优化的代码相同。

由于现代计算机的并发性,对程序执行顺序的假设往往是错误的。对全局变量的访问可能会导致非确定性的结果,即使是在编译器优化被禁用的情况下,仍然有可能引入同步bug。

需要注意的是,由于Rust的安全保证,访问全局(静态)变量需要不安全的代码,假设我们没有使用本模块中的任何同步基元,那么访问全局(静态)变量就需要不安全的代码。

越级执行

由于各种原因,指令的执行顺序可能与我们定义的顺序不同。

  • 编译器重排序:如果编译器可以在更早的时候发出指令,它就会尝试这样做。例如,它可能会在代码块的顶部吊起内存负载,这样CPU就可以开始从内存中预取值。

  • 在单线程场景中,当编写信号处理程序或某些类型的低级代码时,这可能会引起问题。使用编译器栅栏来防止这种重新排序。

  • 单个处理器执行指令的顺序失序。现代CPU能够超尺度执行,即可能同时执行多条指令,即使机器代码描述的是一个顺序过程。

这种重排序是由CPU透明地处理的。

  • 一个多处理器系统同时执行多个硬件线程的情况。在多线程场景中,可以使用两种基元来处理同步问题。
    • 内存栅栏,确保内存访问以正确的顺序被其他CPU看到。
    • 二是原子操作,以确保同时访问同一内存位置不会导致非定义行为。

更高层次的同步对象

大多数低级的同步原语都相当容易出错,使用起来也不方便,这也是为什么标准库也暴露了一些更高级别的同步对象的原因。

这些抽象对象可以从低级原语中构建出来。为了提高效率,标准库中的同步对象通常是在操作系统内核的帮助下实现的,当线程在获取锁时被阻塞时,内核能够对线程进行重新安排。

下面是可用的同步对象的概述:

  • Arc:原子引用计数(Atomically Reference-Counted)指针,可以在多线程环境下使用,以延长某些数据的使用寿命,直到所有线程都使用完为止。

  • Barrier:屏障。确保多个线程相互等待对方到达程序中的某个点,然后再一起继续执行。

  • Condvar:条件变量,提供在等待事件发生时阻止一个线程的能力。

  • mpsc:多生产者,单消费(Multi-producer, single-consumer)队列,用于基于消息的通信。可以提供轻量级的线程间同步机制,代价是增加一些额外的内存。

  • 互斥机制(Mutex)。互斥机制,确保每次最多只有一个线程能够访问一些数据。

  • Once:用于全局变量的线程安全的一次性初始化。

  • RwLock:用于全局变量的初始化。提供了一个相互排斥机制,允许多个读同时访问,同时一次只允许一个写。在某些情况下,这可能比mutex更有效。