2017-08-14 87 views
9

我发现this article,但它看起来不对,因为Cell不保证在锁下的set()和锁上的get()之间的同步。在Rust中写入双重检查锁定的正确方法是什么?

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

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

+3

你的问题是一个合理的问题,但它听起来像[X-Y问题](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)。您可能希望调查像['Once'](https://doc.rust-lang.org/std/sync/struct.Once.html)和[lazy-static](https://crates.io/)这样的组件板条箱/懒惰静态)演示[这里](https://stackoverflow.com/q/27791532/155423)和[这里](https://stackoverflow.com/q/27221504/155423)。 – Shepmaster

回答

11

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

是。

其实,主要原因Ordering存在是强加给非原子读一些排序保证和写道:

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

宽松

越少约束Ordering;不能被重新排序的操作只有在相同的原子值操作:

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

保证打印4。如果另一个线程读取atomic4,则不能保证other是否为8

推出/获取

写入和读出的障碍,分别为:

  • 推出是与store操作中使用,并保证事先写入被执行时,
  • 采集将与load操作一起使用,并保证进一步的读取将会看到至少一个值如同在相应的store之前写的那样。

所以:

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

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

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

保证one1,并且一无所知two说。

。注意,Relaxed商店与Acquire负荷或Release存储与Relaxed负载本质上是无意义的。

注意防锈提供AcqRel:它表现为Release商店和Acquire的负荷,所以你不必记住哪个是哪个?我不建议这样做虽然,因为提供的担保是如此不同。

SeqCst

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


什么是写双重检查锁定在鲁斯特的正确方法?

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

的想法是有3个:

  • 标志,最初为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() 
    } 
} 

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

(1)如果为true,则返回值的引用,该引用必须引用有效内存。这要求在原子转换为真之前执行对该存储器的写入操作,并且只有在该真值为真后才能执行该存储器的读取操作。因此(1)要求Acquire和(3)要求Release。 (2)另一方面没有这样的约束,因为锁定一个Mutex相当于一个完全的内存障碍:所有的写入都保证在之前发生,所有的读取只会在之后发生。因此,此负载不需要进一步的保证,所以Relaxed是最优化的。

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


对于进一步阅读,我真的推荐the article by Preshing这是链接在你链接的一块。它显着地突出了理论(围墙)和实践(被降低到围墙的原子装载/商店)之间的差异。

+0

非常感谢您。但我看到一个错误(可能是我错了)。最后一次读取* self.value.get()在self.initialized.load(Ordering :: Acquire)== true的情况下查看写入操作。出于你提到的原因。但是,如果初始化== false,则在已发布的商店和非原子读取* self.value.get()之间没有获得的负载。例如,在Java中,这是导致值变化的原因(SeqCst)。 –

+0

@АлександрМеньшиков:我不认为这是一个错误。请注意,只有当'self.initialized'为true时,才会加载'Acquire'语义:首先加载并知道它是true还是false。因此,即使它最终评估为“false”,也可以使用“Acquire”语义来执行它,这些语义会在评估它之后对读取进行排序(并在存储之前用'Release'语义进行排序)。 –

+0

是的,但是在具有'Release'语义的商店之后,我们不会在同一个线程**中使用'Acquire' **加载,并且只是在比赛中读取返回的值。并且这个读取没有看到写入,因为在任何写入之前调用了“Acquire”语义加载。可能是Rust对序列的保证,所有的读写操作都在共享内存的同一线程中 - 那么你是对的。只是Java不。这是我怀疑的原因。 –

相关问题