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

Але*_*ков 8 concurrency double-checked-locking rust

我发现了这篇文章,但它看起来不对,因为Cell它不能保证set()锁定和锁定之间的同步get().

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

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

Mat*_* M. 12

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

是.

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

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

轻松

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

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

保证打印4.如果另一个线程读取atomic4,它不知道是否保证other8或不是.

发布/采集

写和读障碍分别为:

  • Release将与store操作一起使用,并保证执行先前的写入,
  • 获取将与load操作一起使用,并保证进一步读取将看到至少与对应之前写入的值一样新的值store.

所以:

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

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

println!("{} {}", one, two);
Run Code Online (Sandbox Code Playgroud)

保证即one1,并且没有任何说明two.

请注意,Relaxed带有Acquire负载的Release商店或带有负载的商店Relaxed基本上没有意义.

请注意,Rust提供AcqRel:它的行为与Release商店和Acquire加载相同,因此您不必记住哪个是...我不推荐它,因为提供的保证是如此不同.

SeqCst

最受约束的Ordering.保证一次性跨所有线程排序.


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

因此,双重检查锁定是利用这些原子操作来避免在不必要时锁定.

想法是有3件:

  • 一旦执行了动作,一个标志,最初为假,并且为真,
  • 一个互斥锁,以保证初始化期间的排除,
  • 要初始化的值.

并使用它们:

  • 如果标志为true,则值已初始化,
  • 否则,锁定互斥锁,
  • 如果标志仍为false:初始化并将标志设置为true,
  • 释放锁定,现在初始化值.

困难在于确保非原子读/写正确排序(并以正确的顺序显示).从理论上讲,你需要完全围栏; 在实践中遵循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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


为了进一步阅读,我真的推荐Preshing的文章,它与您链接的文章相关联.它显着突出了理论(围栏)和实践(原子载荷/商店降低到围栏)之间的差异.