在 Rust 中编写双重检查锁定的正确方法是什么?

What is the right way to write double-checked locking in Rust?

我找到了 this article,但它看起来不对,因为 Cell 不能保证锁下的 set() 和锁上的 get() 之间的同步。

Atomic_.store(true, Ordering::Release)是否影响其他非原子写操作?

我试着用看起来接近Java风格的AtomicPtr来写,但失败了。我找不到在这种情况下正确使用 AtomicPtr 的示例。

Does Atomic_.store(true, Ordering::Release) affect other non-atomic write operations?

是的。

实际上,Ordering 存在的主要原因是对非原子读写施加一些顺序保证:

  • 在同一个执行线程中,对于编译器和CPU,
  • 以便其他线程可以保证它们看到更改的顺序。

放松

约束越少Ordering;唯一不能重新排序的操作是对相同原子值的操作:

atomic.set(4, Ordering::Relaxed);
other = 8;
println!("{}", atomic.get(Ordering::Relaxed));

保证打印4。如果另一个线程读取 atomic4,它无法保证 other 是否是 8

Release/Acquire

写入和读取障碍分别为:

  • 释放store操作一起使用,并保证执行先前的写入,
  • Acquire 将与 load 操作一起使用,并保证进一步的读取将看到至少与相应 [= 之前​​写入的值一样新鲜的值21=].

所以:

// thread 1
one = 1;
atomic.set(true, Ordering::Release);
two = 2;

// thread 2
while !atomic.get(Ordering::Acquire) {}

println!("{} {}", one, two);

保证 one1,并且对 two 只字不提。

请注意,具有 Acquire 负载的 Relaxed 存储或具有 Relaxed 负载的 Release 存储基本上没有意义。

请注意,Rust 提供 AcqRel:它对存储的行为是 Release,对于加载的行为是 Acquire,因此您不必记住哪个是哪个……我知道但是不推荐它,因为提供的保证是如此不同。

SeqCst

最受限制Ordering。保证一次跨所有线程排序。


What is the right way to write double-checked locking in Rust?

所以,双重检查锁定就是利用那些原子操作来避免不必要的锁定。

想法是有3件:

  • 一个标志,最初为 false,一旦执行动作就为 true,
  • 一个互斥锁,以保证初始化期间的排除,
  • 一个值,待初始化。

并这样使用它们:

  • 如果标志为真,值已经初始化,
  • 否则,锁定互斥量,
  • 如果标志仍然为假:初始化并将标志设置为真,
  • 释放锁,值初始化完成

困难在于确保非原子 reads/writes 被正确排序(并以正确的顺序显示)。从理论上讲,您需要完整的围栏;在实践中,遵循 C11/C++11 内存模型的习语就足够了,因为编译器 必须 使其工作。

让我们先检查代码(简化):

struct Lazy<T> {
    initialized: AtomicBool,
    lock: Mutex<()>,
    value: UnsafeCell<Option<T>>,
}

impl<T> Lazy<T> {
    pub fn get_or_create<'a, F>(&'a self, f: F) -> &'a T
    where
        F: FnOnce() -> T
    {
        if !self.initialized.load(Ordering::Acquire) { // (1)
            let _lock = self.lock.lock().unwrap();

            if !self.initialized.load(Ordering::Relaxed) { // (2)
                let value = unsafe { &mut *self.value.get() };
                *value = Some(f(value));
                self.initialized.store(true, Ordering::Release); // (3)
            }
        }

        unsafe { &*self.value.get() }.as_ref().unwrap()
    }
}

有3个原子操作,通过注释编号。我们现在可以检查每种内存排序必须提供的正确性保证。

(1) 如果为真,则返回对值的引用,它必须引用有效的内存。这要求在原子变为真之前执行对该内存的写入,并且只有在它为真之后才执行对该内存的读取。因此 (1) 需要 Acquire 而 (3) 需要 Release.

另一方面,

(2) 没有这样的约束,因为锁定 Mutex 相当于一个完整的内存屏障:保证所有写入都发生在之前,所有读取只会发生在之后。因此,此负载不需要进一步保证,因此 Relaxed 是最优化的。

因此,就我而言,这种双重检查的实施在实践中看起来是正确的。


为了进一步阅读,我真的推荐 the article by Preshing,它在您链接的文章中有链接。它特别突出了理论(栅栏)和实践(原子 loads/stores 降低为栅栏)之间的差异。