此“原子” Rust代码与其“非原子”对应物有什么区别?

时间:2018-12-03 05:18:46

标签: rust atomic

我对Rust还是很陌生。我4年前获得了计算机工程学位,并且记得在我的操作系统课程中讨论(和理解)原子操作。但是,自毕业以来,我一直主要使用高级语言工作,而不必关心原子等低级工作。既然我进入了Rust,我正在努力地记住很多东西是如何工作的。

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

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

这里是add(verbatim):

/// 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(verbatim):

/// 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之后就了解顺序,但是它们仍然使用我不理解的概念。当他们谈论一个线程从另一个线程“看到”某些东西时,这到底是什么意思?如果说顺序一致的操作在“所有线程”中具有相同的顺序,那甚至意味着什么?处理器是否针对不同的线程更改指令顺序?

1 个答案:

答案 0 :(得分: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]到存储tmp1的那一刻之间更改tmp2。因此,如果另一个线程尝试同时设置另一个位,则可能会发生以下序列:

  • 线程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的原因,即使看起来已经预先设置了该位也是如此。