簡體   English   中英

如何在多個線程中加載之前同步存儲?

[英]How do I synchronize a store before a load in multiple threads?

考慮以下程序:

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

int x = 0;
std::atomic<int> y = {0};
std::atomic<bool> x_was_zero = {false};
std::atomic<bool> y_was_zero = {false};

void write_x_load_y()
{
    x = 1;
    if (y == 0)
        y_was_zero = true;
}

void write_y_load_x()
{
    y = 1;
    if (x == 0)
        x_was_zero = true;
}

int main()
{
    std::thread a(write_x_load_y);
    std::thread b(write_y_load_x);
    a.join();
    b.join();
    assert(!x_was_zero || !y_was_zero);
}
  1. 考慮到除了訪問x之外一切都可以是原子的約束,我如何保證斷言通過?
  2. 如果這不可能,如果訪問x可以是原子的但不比“放松”強,是否有可能?
  3. 保證這一點所需的最少同步量(例如,所有操作的最弱 memory 模型)是多少?

我的理解是,如果沒有任何形式的柵欄或原子訪問,存儲x = 1有可能(如果只是理論上如此)低於負載y == 0 (如果不是由編譯器本身移動,則由 CPU 移動) ),導致 x 和 y 都為 0 的潛在競爭(並觸發該斷言)。

我最初的天真印象是 SEQ_CST 保證非原子變量的完全排序。 也就是說,保證在 SEQ_CST 加載y之前排序的x的非原子(或寬松)存儲實際上首先發生; 類似地,在x的非原子(或松弛)加載之前排序的y的 SEQ_CST 存儲保證首先實際發生; 放在一起會阻止比賽。 但是,在進一步閱讀https://en.cppreference.com/w/cpp/atomic/memory_order時,我認為文檔實際上並未說明這一點,而是僅在相反的情況下才能保證這種排序(之前加載商店),或者對xy的訪問都是 SEQ_CST 的情況。

同樣,我天真地認為 memory 屏障會強制屏障之前的所有加載或存儲在其之后的所有加載或存儲之前發生,但閱讀https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence似乎這意味着它再次僅適用於強制在屏障之前對負載進行排序,然后是商店。 我認為,這在這里也無濟於事,除非我應該在比“商店和負載之間”更不明顯的地方設置障礙。

我應該在這里使用什么同步方法? 甚至可能嗎?

這個想法有致命的缺陷,並且不可能在 ISO C++ 中使用非原子x來保證安全。 數據爭用未定義行為 (UB) 是不可避免的,因為一個線程無條件地寫入x ,而另一個線程無條件地讀取它。

充其量你會通過使用編譯器障礙來強制一個線程同步實際的 memory state 與抽象機 state 來滾動你自己的原子。 但即便如此,在沒有 volatile 的情況下滾動您自己的 atomics 也不是很安全: https://lwn.net/Articles/793253/解釋了為什么 Linux 內核的手動滾動 atomics 使用volatile casts 進行純存儲和純加載。 這在普通編譯器上為您提供了類似輕松原子的東西,但當然來自 ISO C++ 的零保證。

何時在多線程中使用 volatile? 基本上永遠不會 - 您可以通過使用atomic<int>mo_relaxed獲得相同的高效 asm。 (或者在 x86 上,即使在 asm 中獲取和釋放也是免費的。)

如果您要嘗試這樣做,實際上在大多數實現中, std::atomic_thread_fence(std::memory_order_seq_cst)將阻止其上非原子操作的編譯時重新排序。 (例如在 GCC 中,我認為它基本上等同於 x86 asm("mfence"::: "memory") 1 ,它阻止編譯時重新排序並且也是一個完整的障礙。但我認為其中一些“強度”是一種實現 -細節,ISO C++ 不要求。

腳注1:順便說一句,通常你想要一個虛擬lock add堆棧memory,而不是實際的mfence,因為mfence更慢。


半相關:您的 bool 變量不需要是原子的。 IDK 如果使它們成為原子或多或少會分散注意力; 如果他們不是,我傾向於更簡單。 它們每個都由最多 1 個線程編寫,並且僅在該線程被join后讀取。 您可以將它們設為純布爾值,也可以無條件地將它們y_was_zero = (y == 0); 如果你想。 (但就簡單性而言,這是中性的,盡管省去了查看它們的初始化程序)。


  1. 保證這一點所需的最少同步量(例如,所有操作的最弱 memory 模型)是多少?

x需要是atomic<>並且兩個存儲都需要是 seq_cst。 (這基本上相當於在進行存儲后排空存儲緩沖區)。

就像在https://preshing.com/20120515/memory-reordering-caught-in-the-act/

在實踐中,我認為在大多數機器上都可以relaxed這兩種負載(盡管可以進行私有存儲轉發,但可能不是 POWER)。 對於 ISO C++ 來保證它,我認為您在兩個負載上也需要 seq_cst ,因此所有 4 個操作都是與程序順序兼容的多個對象的全局總操作順序的一部分。 沒有通過發布/獲取同步來創建發生前的關系。

Generally seq_cst is the only ordering in the ISO C++ memory model that must translate to blocking StoreLoad reordering in a memory model based on the existence of an actual coherent state that exist even if nobody's looking at it, and individual threads accessing that state with local reordering . (ISO C++ 只討論了其他線程可以觀察到的內容,理論上假設的觀察者可能不會限制代碼生成。但實際上他們這樣做是因為編譯器不進行整個程序的線程間分析。)


如果你因為某種原因不能讓x成為atomic<>

使用 C++20 atomic_ref<>構造對x的引用,您可以使用它來執行xref.store(1, mo_seq_cst)xref.load(mo_seq_cst)

或者使用 GNU C/C++ atomic builtins , __atomic_store_n(&x, 1, __ATOMIC_SEQ_CST) (這正是 C++20 atomic_ref 旨在包裝的內容。)

或者使用半便攜的東西, *(volatile int*)&x = 1; 和一個屏障,它可能起作用也可能不起作用,具體取決於編譯器。 如果需要,DeathStation 9000 肯定可以使volatile int 分配成為非原子的。 但幸運的是,人們選擇在現實生活中使用的編譯器並不可怕,而且通常可用於低級系統編程。 盡管如此,任何工作都不能保證這一點。

暫無
暫無

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

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