簡體   English   中英

原子操作傳播/可見性(原子負載與原子 RMW 負載)

[英]Atomic operation propagation/visibility (atomic load vs atomic RMW load)

語境

我正在用 C++ 編寫一個線程安全的原型線程/協程庫,並且我正在使用原子來使任務切換無鎖。 我希望它盡可能高效。 我對原子和無鎖編程有一個大致的了解,但我沒有足夠的專業知識來優化我的代碼。 我做了很多研究,但很難找到我的具體問題的答案:不同內存順序下不同原子操作的傳播延遲/可見性是多少?

當前假設

我讀到對內存的更改是從其他線程傳播的,它們可能會變得可見:

  1. 以不同的順序給不同的觀察者,
  2. 一些延遲。

我不確定這種延遲的可見性和不一致的傳播是否僅適用於非原子讀取,或者也適用於原子讀取,這可能取決於使用的內存順序。 當我在 x86 機器上開發時,我無法在弱有序系統上測試行為。

無論操作類型和使用的內存順序如何,所有原子讀取都總是讀取最新值嗎?

我很確定所有讀-修改-寫(RMW) 操作總是讀取任何線程寫入的最新值,而不管使用的內存順序如何。 對於順序一致的操作似乎也是如此,但前提是對變量的所有其他修改也是順序一致的 據說兩者都很慢,這對我的任務不利。 如果不是所有原子讀取都獲得最新值,那么我將不得不使用 RMW 操作來讀取原子變量的最新值,或者在 while 循環中使用原子讀取,以我目前的理解。

寫入的傳播(忽略副作用)是否取決於內存順序和使用的原子操作?

(僅當上一個問題的答案是並非所有原子讀取總是讀取最新值時,此問題才有意義。請仔細閱讀,我在這里不詢問副作用的可見性和傳播。我只關心原子變量本身的值。)這意味着根據用於修改原子變量的操作,可以保證任何后續原子讀取接收變量的最新值。 因此,我必須在保證始終讀取最新值的操作之間進行選擇,或者使用寬松的原子讀取,以及這種特殊的寫入操作,以保證對其他原子操作的修改的即時可見性。

原子鎖是免費的嗎?

首先,讓我們擺脫房間里的大象:在代碼中使用atomic並不能保證實現無鎖。 atomic只是無鎖實現的推動者。is_lock_free()會告訴你它對於 C++ 實現和你正在使用的底層類型是否真的是無鎖的。

最新值是多少?

在多線程世界中,“最新”一詞非常含糊。 因為操作系統可能使一個線程處於休眠狀態的“最新”線程可能不再是另一個處於活動狀態的線程的最新線程。

std::atomic僅通過確保在一個線程中的一個原子上執行的R、M 和 RMW以原子方式執行,沒有任何中斷,並且所有其他線程看到之前的值或之后的值,從而僅保證對競爭條件的保護,但永遠不會介於兩者之間。 因此, atomic同步線程通過在同一原子對象上的並發操作之間創建順序來實現。

您需要將每個線程視為具有自己時間的平行宇宙,而您不知道平行宇宙中的時間。 就像在量子物理學中一樣,你在一個線程中唯一能知道關於另一個線程的是你可以觀察到的東西(即宇宙之間“之前發生過”的關系)。

這意味着您不應該設想多線程時間,就好像所有線程中都有絕對的“最新”時間一樣。 您需要考慮相對於其他線程的時間。 這就是為什么原子不會創建絕對最新的,而只會確保原子將具有的連續狀態的順序排序。

傳播

傳播不依賴於內存順序,也不依賴於執行的原子操作。 memory_order是關於圍繞原子操作的非原子變量的順序約束,就像柵欄一樣。 對其工作原理的最佳解釋當然是Herb Sutters 演示,如果您正在研究多線程優化,那絕對值得花一個半小時。

盡管特定的 C++ 實現可能會以影響傳播的方式實現某些原子操作,但您不能依賴於您所做的任何此類觀察,因為無法保證傳播在下一個版本中以相同的方式工作編譯器或另一個 CPU 架構上的另一個編譯器。

但是傳播重要嗎?

設計無鎖算法時,很容易通過讀取原子變量來獲取最新狀態。 但是,盡管這種只讀訪問是原子的,但緊隨其后的操作卻不是。 所以下面的指令可能假設一個已經過時的狀態(例如,因為線程在原子讀取后立即發送睡眠)。

if(my_atomic_variable<10)為例,假設您讀取的是 9。假設您處於最佳狀態,並且 9 將是所有並發線程設置的絕對最新值。 將它的值與<10比較不是原子的,所以當比較成功並且if分支時, my_atomic_variable可能已經有了一個新的 10 值。 無論傳播速度有多快,即使讀取將保證始終獲得最新值。 我什至還沒有提到ABA問題

讀取的唯一好處是避免數據競爭和 UB。 但是,如果您想跨線程同步決策/操作,則需要使用 RMW,例如比較和交換(例如atomic_compare_exchange_strong ),以便原子操作的排序產生可預測的結果。

多線程是令人驚訝的領域。 首先,原子讀不是在寫之后排序的。 我讀了一個值並不意味着它以前被寫過。 有時,這種讀取可能會看到(間接,由其他線程)同一線程的某些后續原子寫入的結果。

順序一致性顯然與可見性和傳播有關。 當一個線程寫入一個原子性的“順序一致”時,它會使其之前的所有寫入對其他線程可見(傳播)。 在這種情況下,(順序一致的)讀取是相對於寫入進行排序的。

通常,性能最高的操作是“寬松的”原子操作,但它們對排序提供了最低限度的保證。 原則上有一些因果悖論...... :-)

經過一些討論,這里是我的發現:首先,讓我們定義原子變量的最新值意味着什么:在掛鍾時間,從外部觀察者的角度來看,對原子變量的最新寫入。 如果有多個同時的最后寫入(即在同一周期內在多個內核上),那么選擇其中的哪一個並不重要。

  1. 任何內存順序的原子加載都不能保證讀取最新值。 這意味着寫入必須先傳播,然后才能訪問它們。 這種傳播可能在執行順序方面是亂序的,也可能在不同的觀察者方面有所不同。

     std::atomic_int counter = 0; void thread() { // Imagine no race between read and write. int value = counter.load(std::memory_order_relaxed); counter.store(value+1, std::memory_order_relaxed); } for(int i = 0; i < 1000; i++) std::async(thread);

    在這個例子中,根據我對規范的理解,即使沒有讀寫執行干擾,仍然可能有多個thread執行讀取相同的值,因此最終counter不會是 1000。這個是因為在使用普通讀取時,雖然保證線程以正確的順序讀取對同一變量的修改(它們不會讀取新值,並且在下一個值讀取舊值),但它們不能保證讀取全局最新值將值寫入變量。

    這產生了相對論效應(如愛因斯坦物理學中的那樣),每個線程都有自己的“真理”,這正是我們需要使用順序一致性(或獲取/釋放)來恢復因果關系的原因:如果我們簡單地使用松弛負載,那么我們甚至可以打破因果關系和明顯的時間循環,這可能是由於指令重新排序與無序傳播相結合而發生的。 內存排序將確保由獨立線程感知的那些獨立現實至少在因果上是一致的。

  2. 原子讀-修改-寫 (RMW) 操作(例如 exchange、compare_exchange、fetch_add 等)保證對上述定義的最新值進行操作。 這意味着寫入的傳播是強制的,並導致對內存的一種通用視圖(如果您所做的所有讀取都來自使用 RMW 操作的原子變量),獨立於線程。 因此,如果您使用atomic.compare_exchange_strong(value,value, std::memory_order_relaxed)atomic.fetch_or(0, std::memory_order_relaxed) ,那么您可以保證看到一個包含所有原子變量的全局修改順序。 請注意,這並不能保證非 RMW 讀取的任何順序或因果關系。

     std::atomic_int counter = 0; void thread() { // Imagine no race between read and write. int value = counter.fetch_or(0, std::memory_order_relaxed); counter.store(value+1, std::memory_order_relaxed); } for(int i = 0; i < 1000; i++) std::async(thread);

    在這個例子中(同樣,假設沒有一個thread()執行相互干擾),在我看來,規范禁止value包含除全局最新寫入值之外的任何內容。 因此, counter最終始終為 1000。

現在,什么時候使用哪種閱讀?

如果您只需要每個線程內的因果關系(可能仍然對發生的順序有不同的看法,但至少每個讀者對世界都有因果一致的看法),那么原子加載和獲取/釋放或順序一致性就足夠了。

但是,如果您還需要重新讀取(以便您絕不能讀取全局(跨所有線程)最新值以外的值),那么您應該使用 RMW 操作進行讀取。 這些本身並不會為非原子和非 RMW 讀取創建因果關系,但是所有線程中的所有 RMW 讀取共享完全相同的世界觀,並且始終是最新的。

所以,總結一下:如果允許不同的世界觀,請使用原子加載,但如果您需要客觀現實,請使用 RMW 加載。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM