【问题标题】:Some confused regarding to Rust memory order关于 Rust 内存顺序的一些困惑
【发布时间】:2020-10-03 07:03:01
【问题描述】:

我有一些关于 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 中的 cmets 中确实指出:

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

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

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

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

【问题讨论】:

    标签: rust memory-barriers


    【解决方案1】:

    所有内存操作...发生在 store Release 之前,将在 load Acquire 之后对内存操作可见。

    只有在获取负载看到来自发布存储的值时才如此。

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

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

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

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

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

    【讨论】:

    • 很好的答案,非常感谢!那么我还有一个问题,``` pub fn is_empty(&self) -> bool { let head = self.head.load(Ordering::SeqCst);让 tail = self.tail.load(Ordering::SeqCst); tail == head} ``` 我们能确保 head 总是在 tail 之前加载吗?
    • @PoppinDouble:是的,或者至少“不迟于”。 SeqCst 已经这样做了;所以只会使第一个负载成为获取负载。 preshing.com/20120913/acquire-and-release-semantics。在一个线程中,Acquire 之后按程序顺序的所有操作也按内存顺序排序。这就是获取的全部意义。
    • 只是想确认一下,所有的内存栅栏,都适用于它之前和之后的所有指令,而不仅仅是相邻的两条指令,对吧?
    • @PoppinDouble:是的,当然。 (请注意,从技术上讲,获取操作(例如加载)并不是“栅栏”。它是单向障碍,与 C++ atomic_thread_fence(memory_order_acquire) which is a 2-way barrier 不同。)但是,是的,获取操作是按顺序排列的。所有其他操作都在同一个线程中,否则它们几乎没用。
    • 知道了,太好了!所以让我们从第一条评论中说同样的例子,``` pub fn is_empty(&self) -> bool { let head = self.head.load(Ordering::Relaxed);让 tail = self.tail.load(Ordering::Relaxed); tail == head} ```,现在我把内存顺序改成宽松的,那么现在我们不确定执行顺序,对吧?使用 Relaxed,在同一个线程中,load tail 可能发生在 head 之前,对吧?
    猜你喜欢
    • 2019-11-07
    • 1970-01-01
    • 1970-01-01
    • 2015-05-14
    • 2014-11-02
    • 2016-09-15
    • 2017-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多