有些人对 Rust 内存顺序感到困惑

Some confused regarding to Rust memory order

我有一些关于Rust内存屏障的问题,让我们看看这个example,基于这个例子,我做了一些修改:

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;

struct UsizePair {
    atom: AtomicUsize,
    norm: UnsafeCell<usize>,
}

// UnsafeCell is not thread-safe. So manually mark our UsizePair to be Sync.
// (Effectively telling the compiler "I'll take care of it!")
unsafe impl Sync for UsizePair {}

static NTHREADS: usize = 8;
static NITERS: usize = 1000000;

fn main() {
    let upair = Arc::new(UsizePair::new(0));

    // Barrier is a counter-like synchronization structure (not to be confused
    // with a memory barrier). It blocks on a `wait` call until a fixed number
    // of `wait` calls are made from various threads (like waiting for all
    // players to get to the starting line before firing the starter pistol).
    let barrier = Arc::new(Barrier::new(NTHREADS + 1));

    let mut children = vec![];

    for _ in 0..NTHREADS {
        let upair = upair.clone();
        let barrier = barrier.clone();
        children.push(thread::spawn(move || {
            barrier.wait();

            let mut v = 0;
            while v < NITERS - 1 {
                // Read both members `atom` and `norm`, and check whether `atom`
                // contains a newer value than `norm`. See `UsizePair` impl for
                // details.
                let (atom, norm) = upair.get();
                if atom != norm {
                    // If `Acquire`-`Release` ordering is used in `get` and
                    // `set`, then this statement will never be reached.
                    println!("Reordered! {} != {}", atom, norm);
                }
                v = atom;
            }
        }));
    }

    barrier.wait();

    for v in 1..NITERS {
        // Update both members `atom` and `norm` to value `v`. See the impl for
        // details.
        upair.set(v);
    }

    for child in children {
        let _ = child.join();
    }
}

impl UsizePair {
    pub fn new(v: usize) -> UsizePair {
        UsizePair {
            atom: AtomicUsize::new(v),
            norm: UnsafeCell::new(v),
        }
    }

    pub fn get(&self) -> (usize, usize) {
        let atom = self.atom.load(Ordering::Acquire); //Ordering::Acquire

        // If the above load operation is performed with `Acquire` ordering,
        // then all writes before the corresponding `Release` store is
        // guaranteed to be visible below.

        let norm = unsafe { *self.norm.get() };
        (atom, norm)
    }

    pub fn set(&self, v: usize) {
        unsafe { *self.norm.get() = v };

        // If the below store operation is performed with `Release` ordering,
        // then the write to `norm` above is guaranteed to be visible to all
        // threads that "loads `atom` with `Acquire` ordering and sees the same
        // value that was stored below". However, no guarantees are provided as
        // to when other readers will witness the below store, and consequently
        // the above write. On the other hand, there is also no guarantee that
        // these two values will be in sync for readers. Even if another thread
        // sees the same value that was stored below, it may actually see a
        // "later" value in `norm` than what was written above. That is, there
        // is no restriction on visibility into the future.

        self.atom.store(v, Ordering::Release); //Ordering::Release
    }
}

基本上我只是把判断条件改成if atom != normgetset方法中的内存顺序

根据我目前的了解,所有的内存操作(1.不需要这些内存操作都在同一个内存位置上操作,2.无论是原子操作还是普通内存操作) 发生在 store Release 之前,将对 load Acquire 之后的内存操作可见。

我不明白为什么 if atom != norm 不总是正确的?实际上,从example中的评论来看,确实指出:

However, no guarantees are provided as to when other readers will witness the below store, and consequently the above write. On the other hand, there is also no guarantee that these two values will be in sync for readers. Even if another thread sees the same value that was stored below, it may actually see a "later" value in norm than what was written above. That is, there is no restriction on visibility into the future.

谁能给我解释一下为什么 norm 可以看到一些“未来价值”?

同样在this c++ example中,是不是同样的原因导致代码中出现这些语句?

v0, v1, v2 might turn out to be -1, some, or all of them.

all the memory operations ... happens before a store Release, will be visible to the memory operation after a load Acquire.

仅当获取负载看到来自发布存储的值时才成立。

如果不是,则发布存储之前的获取负载 运行 是全局可见的,因此没有关于任何事情的保证 运行;你实际上并没有与那个作家同步。 norm 的加载发生在获取加载之后,因此另一个商店可能在该时间间隔内变得全局可见1

此外,norm 存储首先完成2 所以即使 atomnorm 同时加载(例如,通过一个宽原子负载),它仍然有可能看到 norm 尚未被 atom 更新。

脚注 1:(或者对该线程可见,在罕见的机器上可能发生这种情况而不 全局 可见,例如 PowerPC )

脚注 2:唯一实际的 gua运行 发球台是 not-later;它们都可以作为一个更广泛的 t运行 行动在全球范围内可见,例如编译器将被允许将 norm 存储和 atom 存储合并到一个更宽的原子存储中,或者硬件可以通过存储缓冲区中的存储合并来实现。所以可能永远不会有一个时间间隔,你可以观察到 norm 没有被 atom 更新;这取决于实现(硬件和编译器)。

(IDK 什么样的 gua运行tees Rust 在这里给出或者它如何正式定义同步和内存顺序。但是获取和释放同步的基础是相当普遍的。https://preshing.com/20120913/acquire-and-release-semantics/。在 C++ 中在没有实现同步的情况下读取 non-atomic norm 将是 data-race UB(未定义的行为),但当然,当为真实硬件编译时,我描述的效果是在实践中会发生什么,无论源语言是 C++ 或 Rust。)