025 Rust死靈書之原子操作

linghuyichong發表於2021-05-07

本系列錄製的影片主要放在B站上Rust死靈書學習影片

Rust 死靈書相關的原始碼資料在github.com/anonymousGiga/Rustonomi...

Rust的原子操作模型基本和c11的原子操作模型一樣。

編譯器努力地透過各種複雜的變換,儘可能減少資料依賴和消除死程式碼。特別是,它可能會徹底改變事件的順序,或者乾脆讓某些事件永遠不會發生。

x = 1;
y = 3;
x = 2;

編譯器可能的最佳化:

x = 2;
y = 3;

麻煩來自於在記憶體分層模式下的 CPU。你的硬體系統裡確實有一些全域性共享的記憶體空間,但是在各個 CPU 核心看來,這些記憶體都離得太遠,速度也太慢。CPU 希望能在它的本地 cache 裡運算元據,只有在 cache 裡沒有需要的記憶體時才委屈地和共享記憶體打交道.

cache裡的內容需要更新,結果就是硬體不能保證相同的事件在兩個不同的執行緒裡一定有相同的執行順序。

考慮程式碼:

初始狀態: x = 0, y = 1

執行緒1         執行緒2               
y = 3;       if x == 1 {
x = 1;           y *= 2;
             }

可能出現幾種情況:

  • y = 3:執行緒 2 線上程 1 完成之前檢查了 x 的值
  • y = 6:執行緒 2 線上程 1 完成之後檢查了 x 的值
  • y = 2:執行緒 2 看到了 x = 1,但是沒看到 y = 3,接下來用計算結果覆蓋了 y = 3(硬體重排可能創造出來的結果)。

一般硬體排序分兩類:強順序和弱順序。

  • 在強順序硬體上要求強順序保證的開銷很小,甚至可能為零,因為硬體本身已經無條件提供了強保證。而弱保證可能只能在弱順序硬體上獲得效能優勢;

  • 在強順序硬體上要求過於弱的順序保證有可能也會碰巧成功,即使你的程式是錯誤的。

    程式的正確執行必須要有時間吸納後的關係,我們透過“資料訪問”和“原子訪問”來控制這種關係。

    資料訪問是程式設計的基礎,它們是非同步的,編譯器最佳化時,可能認定資料訪問都是單執行緒的,可以對它進行任意重排。

    只依靠資料訪問是不可能寫出正確的同步程式碼的。

原子訪問告訴硬體和編譯器,程式是多執行緒的,每種原子訪問都關聯一種排序方式,以確定它和其它訪問之間的關係。

Rust暴露的排序方式包括:

  • 順序一致性(SeqCst);
  • 釋放(Release);
  • 獲取(Acquire);
  • 鬆散(Releaxed)。

順序一致性

  • 順序一致性操作不能被重排,同一個執行緒中,SeqCst之前的訪問永遠在它之前,之後的訪問永遠在它之後;

  • 只使用順序一致性原子操作和資料訪問可以構建一個無資料競爭的程式。

  • 順序一致性不是免費的,即使在順序平臺上,順序一致性也會產生記憶體屏障。

  • 順序一致性很少是程式正確性的必要條件。

獲取-釋放

  • 獲取和釋放經常成對出現。它們適用於獲取和釋放鎖,確保臨界區不會重疊。
  • acquire保證在它之後的訪問永遠在它之後,但是在它之前的操作也有可能被重排在它之後。
  • 強順序平臺上,大多數訪問都有釋放和獲取語義,通常是無開銷的。

鬆散

  • Relaxed訪問最弱,可以被隨意重排,沒有先後關係。
  • 在強順序平臺上,使用Relaxed沒有什麼好處,不過在弱順序平臺上,Relaxed可以獲取的開銷最小。
本作品採用《CC 協議》,轉載必須註明作者和本文連結
令狐一衝

相關文章