2017-08-14 85 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不。這是我懷疑的原因。 –

相關問題