Rust std library中的sync相关包

有用的同步原语。

同步的必要性

从概念上讲,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打印行。

至于幕后发生的事情,启用优化后,最终生成的机器代码可能看起来与代码大不相同:

  • 第一个 store toC可能会在 store to Aor之前被移动B, 好像我们已经写过一样C = 4; A = 3; B = 4

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

  • 最终结果可以通过在编译时查看代码来确定,因此常量折叠可能会将整个块变成一个简单的println!("7 4 4").

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

由于现代计算机涉及并发性,对程序执行顺序的假设通常是错误的。即使禁用了编译器优化,访问全局变量也会导致不确定的结果, 并且仍然可能 引入同步错误。

请注意,由于 Rust 的安全保证,访问全局(静态)变量需要unsafe代码,假设我们不使用此模块中的任何同步原语。

乱序执行

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

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

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

  • 单处理器无序执行指令:现代 CPU 能够进行超标量执行,即多个指令可能同时执行,即使机器代码描述的是顺序过程。

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

  • 同时执行多个硬件线程的多处理器系统:在多线程场景中,可以使用两种原语来处理同步:

更高级别的同步对象

大多数低级同步原语都非常容易出错且使用不便,这就是为什么标准库还公开了一些高级同步对象的原因。

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

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

  • Arc:原子引用计数指针,可用于多线程环境中,以延长某些数据的生命周期,直到所有线程都使用完它。

  • Barrier:确保多个线程在继续执行之前相互等待程序中的某个点。

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

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

  • Mutex:互斥机制,保证一次最多有一个线程能够访问一些数据。

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

  • RwLock: 提供互斥机制,允许同时多个读者,而一次只允许一个写者。在某些情况下,这可能比互斥锁更有效。

分类: 默认 标签: 发布于: 2022-07-11 11:10:15, 更新于: 2022-07-11 11:10:15