簡體   English   中英

不能像 store 一樣在 x86 上放松原子 fetch_add 重新排序嗎?

[英]Can't relaxed atomic fetch_add reorder with later loads on x86, like store can?

該程序有時會打印 00,但如果我注釋掉 a.store 和 b.store 並取消注釋 a.fetch_add 和 b.fetch_add 執行完全相同的操作,即都設置 a=1,b=1 的值,我永遠不會得到00 (在 x86-64 Intel i3 上測試,使用 g++ -O2)

我是否遺漏了什么,或者“00”永遠不會按照標准出現?

這是帶有普通存儲的版本,可以打印 00。

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        //a.fetch_add(1,memory_order_relaxed);
        a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        //b.fetch_add(1,memory_order_relaxed);
        b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%d\n",reta,retb);
        return 0;
}

下面從不打印 00

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        a.fetch_add(1,memory_order_relaxed);
        //a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        b.fetch_add(1,memory_order_relaxed);
        //b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%d\n",reta,retb);
        return 0;
}

也看看這個Multithreading atomics ab printing 00 for memory_order_relaxed

該標准允許00 ,但您永遠不會在 x86 上得到它(沒有編譯時重新排序)。 在 x86 上實現原子 RMW 的唯一方法涉及lock前綴,這是一個“完整屏障”,對於 seq_cst 來說足夠強大。

在 C++ 術語中,在為 x86 編譯時,原子 RMW 被有效地提升為 seq_cst (只有在確定了可能的編譯時排序之后——例如,非原子加載/存儲可以通過寬松的 fetch_add 重新排序/組合,其他寬松的操作也可以,以及使用獲取或釋放操作的單向重新排序。盡管編譯器較少可能會相互重新排序原子操作,因為它們不會組合它們,這樣做是編譯時重新排序的主要原因之一。)

實際上,大多數編譯器通過使用xchg (具有隱式lock前綴)來實現a.store(1, mo_seq_cst) ,因為它比現代 CPU 上的mov + mfence更快,並且使用lock add作為唯一寫入將 0 變為 1每個 object 完全相同。 有趣的事實:只需存儲和加載,您的代碼將編譯為與https://preshing.com/20120515/memory-reordering-caught-in-the-act/相同的 asm,因此此處的討論適用。


ISO C++ 允許整個寬松的 RMW 以寬松的負載重新排序,但普通編譯器不會在編譯時無緣無故地這樣做 (DeathStation 9000 C++ 實現可能/將會)。 因此,您終於找到了在不同的 ISA 上進行測試很有用的案例。 原子 RMW(甚至它的一部分)在運行時重新排序的方式很大程度上取決於 ISA。


需要重試循環來實現 fetch_add 的LL/SC機器(例如 ARM 或ARMv8.1 之前的 AArch64 )可能能夠真正實現可以在運行時重新排序的寬松 RMW,因為任何比寬松更強的東西都需要障礙。 (或獲取/發布指令的版本,如AArch64 ldaxr / stlxr vs. ldxr / stxr )。 因此,如果 Relaxed 和 acq 和/或 rel 之間存在 asm 差異(有時 seq_cst 也不同),那么這種差異很可能是必要的,並且會阻止一些運行時重新排序。

即使是單指令原子操作也可能在 AArch64 上真正放松; 我沒有調查過。 大多數弱序 ISA 傳統上使用 LL/SC 原子,所以我可能只是將它們混為一談。

在 LL/SC 機器中,LL/SC RMW 的存儲端甚至可以與以后的加載分開重新排序,除非它們都是 seq_cst。 出於排序的目的,原子讀取-修改-寫入操作是一個還是兩個?


要真正看到00 ,兩個加載都必須在 RMW 的存儲部分在另一個線程中可見之前發生。 是的,我認為 LL/SC 機器中的硬件重新排序機制與重新排序普通商店非常相似。

這個問題的關鍵是要意識到寬松的 memory 排序不能保證線程之間的同步:

標記為 memory_order_relaxed 的原子操作不是同步操作; 它們不會在並發 memory 訪問中強加順序 它們只保證原子性和修改順序的一致性。

因此,在第一個代碼中,可能會發生不同的情況。 例如:

  • 首先執行foo()線程中的代碼,然后執行bar()線程: retb是 0, reta是 1,所以你會得到 10。
  • 首先執行bar()線程中的代碼,然后執行foo()線程: reta是 0, retb是 1,所以你會得到 01。
  • foo()bar()的線程中的代碼同時逐條執行。 那么retaretb都是 1,你會得到 11。
  • 寬松的 memory 排序還允許不同步的情況:兩個線程都更新它們的原子並查看它們當前的原子值,但看到另一個線程的未同步值(即原子更改之前的值)。 所以你可以retaretb at 0 得到你 00。

第二個代碼也遇到了同樣的問題,因為它是寬松的排序,並且用於設置retaretb的訪問是對另一個線程中修改的原子的只讀訪問。 你可以擁有所有四種可能性。

如果要確保按預期進行同步,則需要確保所有原子操作之間的全局順序,因此 go 與memory_order_seq_cst 這將排除 00,但仍保留所有其他可能的組合。

(注意:我以前使用memory_order_acquire的建議確實不夠,因為它仍然保證線程之間沒有順序以進行不同原子的操作,正如 Peter 在評論中解釋的那樣)

在這兩種情況下我都得到“10”。 第一個線程總是運行得更快,並且a == 1 但是如果你給foo()添加額外的操作

#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){

    int i=0;
    while(i < 10000000)
        i++;

    a.fetch_add(1,memory_order_relaxed);
    //a.store(1,memory_order_relaxed);
    retb=b.load(memory_order_relaxed);
}

void bar(){
    b.fetch_add(1,memory_order_relaxed);
    //b.store(1,memory_order_relaxed);
    reta=a.load(memory_order_relaxed);
}

int main(){
    thread t[2]{ thread(foo),thread(bar) };
    t[0].join(); t[1].join();
    printf("%d%d\n",reta,retb);
    return 0;
}

您將收到“01”!

暫無
暫無

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

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