簡體   English   中英

memory_order_consume 和 memory_order_acquire 的區別

[英]Difference between memory_order_consume and memory_order_acquire

我有一個關於GCC-Wiki 文章的問題 在標題“總體摘要”下給出了以下代碼示例:

主題 1:

y.store (20);
x.store (10);

主題 2:

if (x.load() == 10) {
  assert (y.load() == 20)
  y.store (10)
}

據說,如果所有 store 都是release並且所有 load 都是acquire ,那么線程 2 中的斷言不會失敗。 這對我來說很清楚(因為線程 1 中 x 的存儲與線程 2 中 x 的負載同步)。

但現在到了我不明白的部分。 也有人說,如果所有 store 都是release並且所有 load 都是consumer ,結果是一樣的。 來自 y 的負載是否可能在來自 x 的負載之前提升(因為這些變量之間沒有依賴關系)? 這意味着線程 2 中的斷言實際上可能會失敗。

C11 標准的裁決如下。

5.1.2.4 多線程執行和數據競爭

  1. 評估 A16)評估 B之前依賴順序的,如果:

    A 對原子對象 M 執行釋放操作,並且,在另一個線程中,B 對 M 執行消耗操作並讀取以 A 為首的釋放序列中任何副作用寫入的值,或

    — 對於某些評估 X,A 在 X 之前是依賴順序的,並且 X 攜帶對 B 的依賴。

  2. 如果 A 與 B 同步,A 在 B 之前進行依賴排序,或者對於某些評估 X,則評估 A線程間發生在評估 B 之前:

    — A 與 X 同步,X 在 B 之前排序,

    — A 在 X 之前被排序並且 X 線程間在 B 之前發生,或者

    — A 線程間發生在 X 之前,X 線程間發生在 B 之前。

  3. 注 7:“線程間發生在之前”關系描述了“先於排序”、“與同步”和“依賴順序先於”關系的任意串聯,但有兩個例外 第一個例外是,串聯不允許以“dependency-ordered before”后跟“sequenced before”結尾。 這種限制的原因是參與“依賴順序在前”關系的消費操作僅提供與該消費操作實際攜帶依賴關系的操作相關的排序。 此限制僅適用於此類串聯的結尾的原因是任何后續發布操作都將為之前的消費操作提供所需的排序。 第二個例外是不允許串聯完全由“先於序列”組成。 這種限制的原因是 (1) 允許“線程間發生在之前”被傳遞性地關閉,以及 (2) 下文定義的“發生在之前”關系提供了完全由“之前發生”組成的關系''。

  4. 如果 A 在 B 之前排序A 線程間在 B之前發生,則評估 A評估 B 之前發生。

  5. 對象 M 上的可見副作用 A相對於 M 的值計算 B 滿足條件:

    A 發生在 B 之前,並且

    — 沒有 X 對 M 的其他副作用,使得 A 發生在 X 之前,X 發生在 B 之前。

    由評估 B 確定的非原子標量對象 M 的值應為可見副作用 A 存儲的值。

(強調)


在下面的評論中,我將縮寫如下:

  • 依賴排序之前: DOB
  • 線程間發生之前: ITHB
  • 之前發生過: HB
  • 前序: SeqB

讓我們回顧一下這是如何應用的。 我們有 4 個相關的內存操作,我們將它們命名為評估 A、B、C 和 D:

主題 1:

y.store (20);             //    Release; Evaluation A
x.store (10);             //    Release; Evaluation B

主題 2:

if (x.load() == 10) {     //    Consume; Evaluation C
  assert (y.load() == 20) //    Consume; Evaluation D
  y.store (10)
}

為了證明 assert 永遠不會跳閘,我們實際上試圖證明A 始終是 D 處的可見副作用 根據5.1.2.4 (15),我們有:

A SeqB B DOB C SeqB D

這是一個以 DOB 結尾的串聯,然后是 SeqB。 盡管 (16) 說了些什么,但 (17)明確規定這不是ITHB 串聯。

我們知道,由於A和D不在同一個執行線程中,所以A不是SeqB D; 因此,對於 HB,(18)中的兩個條件都不滿足,並且 A 不滿足 HB D。

因此,A 對 D 不可見,因為不滿足(19)的條件之一。 斷言可能會失敗。


然后, 在 C++ 標准的內存模型討論中以及此處的第 4.2 節控制依賴項中描述了是如何進行的

  1. (提前一段時間)線程 2 的分支預測器猜測if將被采用。
  2. 線程 2 接近 predict-taken 分支並開始推測性獲取。
  3. 線程 2 亂序並推測性0xGUNKy加載0xGUNK (評估 D)。 (也許它還沒有從緩存中驅逐?)。
  4. 線程 1 將20存儲到y (評估 A)
  5. 線程 1 將10存儲到x (評估 B)
  6. 線程 2 從x加載10 (評估 C)
  7. 線程 2 確認if被采用。
  8. 線程 2 的y == 0xGUNK推測負載已提交。
  9. 線程 2 失敗斷言。

允許評估 D 在 C 之前重新排序的原因是因為消費沒有禁止它。 這與Acquire-load 不同,它可以防止在程序順序之后的任何加載/存儲之前重新排序。 再次,5.1.2.4(15) 指出,參與“依賴順序在前”關系的消費操作僅提供與該消費操作實際攜帶依賴的操作相關的排序,並且絕對沒有依賴在兩個負載之間。


CppMem 驗證

CppMem是一個工具,可幫助探索 C11 和 C++11 內存模型下的共享數據訪問場景。

對於以下近似問題中場景的代碼:

int main() {
  atomic_int x, y;
  y.store(30, mo_seq_cst);
  {{{  { y.store(20, mo_release);
         x.store(10, mo_release); }
  ||| { r3 = x.load(mo_consume).readsvalue(10);
        r4 = y.load(mo_consume); }
  }}};
  return 0; }

該工具報告了兩個一致的、無競爭的場景,即:

消費,成功場景

其中y=20被成功讀取,並且

消費,失敗場景

其中讀取“過時”初始化值y=30 手繪圓是我的。

相比之下,當使用mo_acquire進行加載時,CppMem 僅報告一種一致的、無競爭的場景,即正確的場景:

收購,成功場景

其中y=20被讀取。

兩者都在原子存儲上建立了可傳遞的“可見性”順序,除非它們已通過memory_order_relaxed發出。 如果線程使用其中一種模式讀取原子對象x ,則可以確保它看到對所有原子對象y所有修改,這些修改是在寫入x之前完成的。

“獲取”和“消耗”之間的區別在於對某些變量z的非原子寫入的可見性,例如。 對於acquire所有寫入,無論是否為原子,都是可見的。 對於consume只有原子的保證是可見的。

thread 1                               thread 2
z = 5 ... store(&x, 3, release) ...... load(&x, acquire) ... z == 5 // we know that z is written
z = 5 ... store(&x, 3, release) ...... load(&x, consume) ... z == ? // we may not have last value of z

暫無
暫無

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

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