首页 > 解决方案 > 关于 Rust 内存顺序的一些困惑

问题描述

我对 Rust 内存屏障有一些疑问,让我们看看这个例子,基于这个例子,我做了一些改变:

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
    }
}

基本上,我只是将判断条件改为and方法if atom != norm中的内存顺序。getset

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

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

但是,不能保证其他读者何时会看到下面的商店,从而看到上面的内容。另一方面,也不能保证这两个值对读者来说是同步的。即使另一个线程看到下面存储的相同值,它实际上也可能看到norm比上面写的值“晚”的值。也就是说,对未来的可见性没有限制。

有人可以向我解释为什么norm可以看到一些“未来价值”吗?

同样在这个 c++ 示例中,导致这些语句出现在代码中的原因是否相同?

v0、v1、v2 可能会变成 -1、部分或全部。

标签: rustmemory-barriers

解决方案


所有内存操作...发生在存储释放之前,将在加载获取后对内存操作可见。

仅当获取负载看到来自发布存储的值时,这才是正确的。

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

此外,norm存储首先完成2因此即使atom同时norm加载 和 (例如,通过一个广泛的原子加载),它仍然有可能看到尚未norm更新。atom

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

脚注 2:唯一的实际保证是不迟到;它们都可以作为一个更广泛的事务变得全局可见,例如允许编译器将norm存储和atom存储合并到一个更广泛的原子存储中,或者硬件可以通过存储缓冲区中的存储合并来做到这一点。因此,您可能永远不会观察到norm更新的时间间隔atom;它取决于实现(硬件和编译器)。

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


推荐阅读