【问题标题】:What is the difference between this "atomic" Rust code and its "non-atomic" counterpart?这个“原子”的 Rust 代码和它的“非原子”对应物有什么区别?
【发布时间】:2019-05-04 09:35:24
【问题描述】:

我对 Rust 还很陌生。 4 年前,我获得了计算机工程学位,我记得在我的操作系统课程中讨论(和理解)原子操作。然而,自从毕业以来,我一直主要使用高级语言工作,我不必关心像原子这样的低级内容。现在我开始接触 Rust,我很难记住这些东西是如何工作的。

我目前正在尝试了解hibitset 库的源代码,特别是atomic.rs

这个模块指定了一个 AtomicBitSet 类型,它对应于 lib.rs 中的 BitSet 类型,但使用原子值和操作。据我了解,“原子操作”是保证不会被另一个线程中断的操作;相同值上的任何“加载”或“存储”都必须等待操作完成才能继续。根据该定义,“原子值”是其操作完全是原子的值。 AtomicBitSet 使用 AtomicUsize,这是一个 usize 包装器,其中所有方法都是完全原子的。但是,AtomicBitSet 指定了几个似乎不是原子的操作(addremove),并且有一个原子操作:add_atomic。看看addadd_atomic,我真的不知道有什么区别。

这里是add(逐字):

/// Adds `id` to the `BitSet`. Returns `true` if the value was
/// already in the set.
#[inline]
pub fn add(&mut self, id: Index) -> bool {
    use std::sync::atomic::Ordering::Relaxed;

    let (_, p1, p2) = offsets(id);
    if self.layer1[p1].add(id) {
        return true;
    }

    self.layer2[p2].store(self.layer2[p2].load(Relaxed) | id.mask(SHIFT2), Relaxed);
    self.layer3
        .store(self.layer3.load(Relaxed) | id.mask(SHIFT3), Relaxed);
    false
}

此方法直接调用load()store()。我假设它使用Ordering::Relaxed 的事实使得这个方法不是原子的,因为另一个线程对不同的索引做同样的事情可能会破坏这个操作。

这里是add_atomic(逐字):

/// Adds `id` to the `AtomicBitSet`. Returns `true` if the value was
/// already in the set.
///
/// Because we cannot safely extend an AtomicBitSet without unique ownership
/// this will panic if the Index is out of range.
#[inline]
pub fn add_atomic(&self, id: Index) -> bool {
    let (_, p1, p2) = offsets(id);

    // While it is tempting to check of the bit was set and exit here if it
    // was, this can result in a data race. If this thread and another
    // thread both set the same bit it is possible for the second thread
    // to exit before l3 was set. Resulting in the iterator to be in an
    // incorrect state. The window is small, but it exists.
    let set = self.layer1[p1].add(id);
    self.layer2[p2].fetch_or(id.mask(SHIFT2), Ordering::Relaxed);
    self.layer3.fetch_or(id.mask(SHIFT3), Ordering::Relaxed);
    set
}

此方法使用fetch_or 而不是直接调用loadstore,我假设这就是使此方法具有原子性的原因。

但是为什么Ordering::Relaxed 的使用仍然允许它被认为是原子的?我意识到单个“或”操作是原子的,但完整的方法可以与另一个线程同时运行。不会有影响吗?

此外,为什么像这样的类型会公开非原子方法?仅仅是为了表现吗?这对我来说似乎很混乱。如果我要选择AtomicBitSet 而不是BitSet,因为它将被多个线程使用,我可能只想对其使用原子操作。如果我不这样做,我就不会使用它。对吧?

我也希望对 add_atomic 中的评论进行解释。按原样对我来说没有意义。非原子版本还不需要关心吗?这两种方法似乎在有效地做同一件事,只是原子性不同。

我真的很想在原子方面得到一些帮助。我认为我在阅读thisthis 后理解了排序,但两者仍在使用我不理解的概念。当他们谈论一个线程“看到”另一个线程时,这到底是什么意思?当说顺序一致的操作“跨所有线程”具有相同的顺序时,这意味着什么?处理器对不同线程改变指令顺序的方式不同吗?

【问题讨论】:

    标签: rust atomic


    【解决方案1】:

    在非原子情况下,这一行:

    self.layer2[p2].store(self.layer2[p2].load(Relaxed) | id.mask(SHIFT2), Relaxed);
    

    或多或少等同于:

    let tmp1 = self.layer2[p2];
    let tmp2 = tmp1 | id.mask(SHIFT2);
    self.layer2[p2] = tmp2;
    

    所以另一个线程可以在self.layer2[p2] 被读入tmp1tmp2 存储到其中之间改变。所以如果另一个线程同时尝试设置另一个位,就有可能出现以下顺序:

    • 线程 1 读取一个空掩码,
    • 线程 2 读取一个空掩码,
    • 线程 1 设置掩码的第 1 位并写入,
    • 线程2设置掩码的第2位并写入,从而覆盖线程1设置的值,
    • 最后只设置了第 2 位!

    self.layer3 也是如此。

    在原子的情况下,fetch_or 的使用保证了整个读取-修改-写入周期是原子的。

    在这两种情况下,由于顺序放宽,对layer2layer3 的写入可能会以任何从其他线程看到的顺序发生。

    add_atomic 中的注释是为了避免两个线程尝试添加相同的位时出现问题。假设add_atomic 是这样写的:

    pub fn add_atomic(&self, id: Index) -> bool {
        let (_, p1, p2) = offsets(id);
    
        if self.layer1[p1].add(id) {
            return true;
        }
    
        self.layer2[p2].fetch_or(id.mask(SHIFT2), Ordering::Relaxed);
        self.layer3.fetch_or(id.mask(SHIFT3), Ordering::Relaxed);
        false
    }
    

    那么您将面临以下风险:

    • 线程 1 设置 layer1 中的位 1 并看到它没有事先设置,
    • 线程 2 尝试设置 layer1 中的位 1,并看到线程 1 已经设置了它,因此线程 2 从 add_atomic 返回,
    • 线程 2 执行了另一个需要读取 layer3 的操作,但是 layer3 还没有更新,所以线程 2 得到了错误的值!
    • 线程1更新layer3,但为时已晚。

    这就是为什么add_atomic 的情况可以确保layer2layer3 在所有线程中都正确设置,即使看起来该位已经预先设置。

    【讨论】:

      猜你喜欢
      • 2011-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-10
      • 2011-04-20
      • 2011-10-28
      • 1970-01-01
      相关资源
      最近更新 更多