1. 程式人生 > >原子變數和記憶體順序

原子變數和記憶體順序

翻譯來源

內容

使用多執行緒很困難,不僅是因為很多事情同時發生,還因為你程式碼中所寫的並不一定是CPU中所發生的。為了獲得更好的效能表現,編譯器在認為沒有監視限制的情況下就會cheat(優化程式碼),他會對指令重新排序,或者忽略一些它認為無用的指令。硬體方面同樣會這樣做,同一時刻同一記憶體地址能夠存在於不同的快取中,但某一快取的修改並不會被立刻更新到其他快取中。如果不小心處理,會導致未定義行為。除了傳統的同步原語(mutexes, barriers, …),一些語言提供了原子型別(C++Rust),這很酷,它允許實現傳統的原語和一些快速的多執行緒資料結構(例如crossbeam)。不幸的是,他們比起在mutex後面放一些程式碼要難用,部分是因為他們神奇的記憶體

ordering parameter,直接影響了他們的記憶體同步。

大多數的文件從保證性、因果性和預見性進行說明,它們擅長形式說明,但使用中項像教一個會計學集合論,並不實用。我能找到的最接近實用的文件是在nomicon,但是我決定自己重新陳述一下,希望沒有錯誤。

原子變數究竟是什麼?

正如其他型別一樣,原子型別一樣它存在普通RAM中,它並不比其他非原子變數多使用記憶體,他們在二進位制形式上是相同的,只是編譯器生成不同的指令來處理它(避免優化)。理論上一個變數有時可以當作是原子變數使用,有時可以不當作原子變數,但實際中意義不大。
然而,編譯器和處理器都希望cheat(你也希望他們cheat,加速執行速度)。 ordering parameter則是說明了什麼樣的cheat是被允許的。這也是原子變數比mutex要快的原因–它從更細的層面上控制了cheat(另一個原因是mutex 進入了核心態掛起其他執行緒,直到解鎖,這很耗時)。

一些手冊推薦當你不確定時,使用順序一致(Sequentially Consistent)的順序,但我人我這不是好建議——並不僅僅是從效能方面。如果你不知道你需要的記憶體順序,你就無法確定該使用原子變數還是mutex ,還是其他的更耗時的東西。原子變數學習的重點並不是原子變數的工作原理,而是在於他的平行研究,他會影響其他記憶體位置的同步,其次才是速度。

Relaxed order ‒ sanity just for me

只有當從不同執行緒中同時訪問變數而不引起未定義操作,此時原子變數才稱得上穩健如果你從0開始,每次增1,執行10次,你會獲得從0到10 的數字,最終也會得到10.執行緒會將對於一個原子變數的操作看成是順序操作(原子變數達到10 ,我的執行緒會看見它,除非有減操作否則不會回到5)。

let mut threads = Vec::new();
for _ in 0..5 {
    threads.push(thread::spawn(|| {
        let first = a.fetch_add(1, Ordering::Relaxed);
        let second = a.fetch_add(1, Ordering::Relaxed);
        assert!(first < second);
    }));
}
for t in threads {
    t.join().unwrap();
}
assert_eq!(10, a.load(Ordering::Relaxed));

除此之外, relaxed order 不保證其他任何東西。無法保證原子變數之間的順序(當遞增原子變數A和B,某一個執行緒看到的是先遞增A後遞增B,另一個執行緒可能看到的是相反的順序),是的不同的執行緒看到的是不同的順序(不要過分在意,感覺有點精神分裂)。如果有多個變數會產生相當怪異的結果。
適用情況:

  • 生成唯一ID
  • 控制其他執行緒(停止其他執行緒)
  • 統計或在程式末尾獲得某個數
  • 出於效能考慮,

並不是所有的架構都有這個順序,沒有的話就會採用限制更強的東西。

Release & Acquire ‒ sending data from one thread to another

除了relaxed order 所提供的,它提供了兩個執行緒間的契約,若一個執行緒release 某個原子變數,另一個執行緒同時acquire原子變數。前一個執行緒在release 之前對原子變數的寫都會被另一個執行緒所acquire。這對操作是移交記憶體的交會點。
適用情況:
- 寫互斥量,自旋鎖,管道,或其他有趣的資料結構。這種成對的操作保證了原始線能程更新RAM和其他執行緒獲取所有相關記憶體,(這是這是對實際發生的事情的一種簡化,存在cache coherence,不必將資料送入RAM)
- 建立雙向同步(mutex 以原子變數的形式獲得鎖,獲得當前的記憶體,修改後釋放它)。建立單向同步(寫者通過釋放操作立刻放棄管道,讀者獲得寫者的內容,從而節約頻寬)。

let spinlock = AtomicBool::new(false); // not locked
...
while spinlock.compare_and_swap(false, true, Ordering::Acquire) {}
// It's locked here
spinlock.store(false, Ordering::Release); 

注意:

  • 並不保證與其他執行緒的同步
  • 並不保證在釋出操作之後,第一個執行緒不會改變記憶體。
  • 不同原子變數的操作沒有聯絡
  • 正確的同步方式是:在release之後第一個執行緒停止寫。
  • 只有當 release 操作出現在寫(store)中,acquire 操作出現在讀中才(load)才起作用。相反則無效。

同樣存在AcqRel順序,它同時可以acquire and release,load-store,(像fetch_add操作)。

Sequentially consistent

它與AcqRel類似(load 時Acquire,store時Release ),但是它同步了其它原子變數,準確來說是SeqCst 操作貫穿整個程式,每個執行緒都有一個單一的操作時間線。較弱的操作仍有可能得到希望的結果。

Other synchronization points

正如AcqRel操作不同執行緒中同一原子變數的更新,也存在其他情況,可能隱藏著一些原子操作。

  • thread/memory模型中需要強制傳播更新: Locking/unlocking 一個mutex,產生臨界區。
  • 用管道傳送資料。
  • Spawning 或 joining 其他的執行緒

總結

Relaxed:放輕鬆,不要擔諸如事物的順序或其他的記憶體等細節。
Release:告知你在記憶體中改變了什麼。
Acquire:獲得所有更新。
SeqCst:確保所有事物的一致性。