簡體   English   中英

理解 C++11 中的 `memory_order_acquire` 和 `memory_order_release`

[英]Understanding `memory_order_acquire` and `memory_order_release` in C++11

我正在閱讀文檔,更具體地說

memory_order_acquire :具有此內存順序的加載操作對受影響的內存位置執行獲取操作:在此加載之前,當前線程中的讀取或寫入不能重新排序。 釋放相同原子變量的其他線程中的所有寫入在當前線程中都是可見的(請參閱下面的 Release-Acquire 排序)。

memory_order_release :具有此內存順序的存儲操作執行釋放操作:在此存儲之后無法重新排序當前線程中的讀取或寫入。 當前線程中的所有寫入在獲取相同原子變量的其他線程中都是可見的(請參閱下面的 Release-Acquire 排序),並且攜帶對原子變量的依賴的寫入在使用相同原子變量的其他線程中變得可見(請參閱 Release-Consume下單)

這兩個位:

memory_order_acquire

...在此加載之前不能重新排序當前線程中的讀取或寫入...

memory_order_release

...在此存儲之后,無法重新排序當前線程中的讀取或寫入...

它們究竟是什么意思?

還有這個例子

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string*> ptr;
int data;

void producer()
{
    std::string* p  = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

但我真的不知道我引用的兩個位在哪里適用。 我明白發生了什么,但我沒有真正看到重新排序的位,因為代碼很小。

一個線程完成的工作不能保證對其他線程可見。

為了使數據在線程之間可見,需要一種同步機制。 可以使用非松弛atomicmutex 它被稱為獲取-釋放語義。 編寫互斥鎖會“釋放”它之前的所有內存寫入,並讀取相同的互斥鎖“獲取”這些寫入。

這里我們使用ptr將迄今為止完成的工作( data = 42 )“釋放”到另一個線程:

    data = 42;
    ptr.store(p, std::memory_order_release); // changes ptr from null to not-null

在這里我們等待它,通過這樣做我們同步(“獲取”)生產者線程完成的工作:

    while (!ptr.load(std::memory_order_acquire)) // assuming initially ptr is null
        ;
    assert(data == 42);

注意兩個不同的操作:

  1. 我們在線程之間等待(同步步驟)
  2. 作為等待的副作用,我們可以工作從提供者轉移到消費者(提供者發布,消費者獲取它)

在沒有 (2) 的情況下,例如當使用memory_order_relaxed ,只有atomic值本身是同步的。 之前/之后完成的所有其他工作都不是,例如data不一定包含42並且地址p處可能沒有完全構造的string實例(如消費者所見)。

有關獲取/釋放語義和 C++ 內存模型的其他詳細信息的更多詳細信息,我建議您在 channel9 上觀看 Herb 出色的atomic<> 武器演講,它很長但很有趣。 有關更多詳細信息,有一本書名為“C++ Concurrency in Action”

如果您將std::memory_order_relaxed用於存儲,則編譯器可以使用“as-if”規則來移動data = 42; 到存儲之后, consumer可以看到非空指針和不確定data

如果您使用std::memory_order_relaxed進行加載,則編譯器可以使用“as-if”規則來移動assert(data == 42); 到加載循環之前。

這兩個都是允許的,因為data的值與ptr的值無關

如果ptr是非原子的,則會出現數據競爭,因此會出現未定義的行為。

獲取和釋放是內存障礙。 如果您的程序在獲取障礙之后讀取數據,您可以確信您將讀取與任何其他線程關於同一原子變量的任何先前版本一致的數據。 原子變量在所有線程中的讀取和寫入保證具有絕對順序(當使用memory_order_acquirememory_order_release盡管提供了較弱的操作)。 這些屏障實際上將該順序傳播到使用該原子變量的任何線程。 您可以使用原子來指示某事已“完成”或“准備好”,但如果消費者讀取超出該原子變量的內容,則消費者不能依賴於“看到”其他內存的正確“版本”,原子將具有有限的價值.

關於“移動之前”或“移動之后”的語句是給優化器的指令,它不應該重新排序操作以無序進行。 優化器非常擅長重新排序指令,甚至省略冗余讀/寫,但如果他們跨內存屏障重新組織代碼,他們可能會在不知不覺中違反該順序。

您的代碼依賴於std::string對象(a)在分配ptr之前已在producer()構造並且(b)該字符串的構造版本(即它占用的內存版本)是consumer()讀。 簡單地說, consumer()會在看到ptr被分配后立即急切地讀取字符串,所以它該死的更好地看到一個有效且完全構造的對象,否則會出現糟糕的情況。 在該代碼中,分配ptr “行為”是producer() “告訴” consumer字符串“准備好”的方式。 內存屏障的存在是為了確保這是消費者看到的。

相反,如果ptr被聲明為普通的std::string *那么編譯器可以決定優化p並將分配的地址直接分配給ptr ,然后才構造對象並分配int數據。 對於使用該分配作為對象producer准備就緒的指示器的consumer線程來說,這可能是一場災難。 准確地說,如果ptr是一個指針, consumer可能永遠不會看到分配的值,或者在某些體系結構上讀取部分分配的值,其中僅分配了一些字節並且它指向垃圾內存位置。 然而,這些方面是關於它是原子的,而不是更廣泛的內存屏障。

暫無
暫無

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

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