簡體   English   中英

寬松的原子存儲是否在發布前重新排序? (類似於加載/獲取)

[英]Are relaxed atomic store reordered themselves before the release? (similar with load /acquire)

我在en.cppreference.com 規范中閱讀了對原子的放寬操作:

“[...]只保證原子性和修改順序的一致性。”

所以,我在問自己,當您處理相同或不同的原子變量時,這種“修改順序”是否有效。

在我的代碼中,我有一個原子樹,其中一個低優先級、基於事件的消息線程使用memory_order_relaxed填充應該更新的節點,並將一些數據存儲在紅色“1”原子(見圖)上。 然后它繼續使用 fetch_or 在其父級中寫入以了解哪個子原子已被更新。 每個原子最多支持 64 位,因此我將位 1 填充為紅色操作“2”。 它一直持續到根原子,它也使用 fetch_or 進行標記,但這次使用的是memory_order_release

在此處輸入圖像描述

然后一個快速、實時、不可阻塞的線程加載控制原子(使用memory_order_acquire )並讀取啟用它的位。 然后它使用memory_order_relaxed遞歸更新子原子。 這就是我將數據與高優先級線程的每個周期同步的方式。

由於該線程正在更新,因此可以將子原子存儲在其父級之前。 問題是在我填寫子信息之前它存儲了一個父項(填充要更新的子項)。

換句話說,正如標題所說,寬松的商店在發布之前是否在它們之間重新排序? 我不介意對非原子變量進行重新排序。 偽代碼,假設 [x, y, z, control] 是原子的並且初始值為 0:

Event thread:
z = 1; // relaxed
y = 1; // relaxed
x = 1; // relaxed;
control = 0; // release

Real time thread (loop):
load control; // acquire
load x; // relaxed
load y; // relaxed
load z; // relaxed

我想知道在實時線程中這是否總是正確的:x <= y <=z。 要檢查我是否編寫了這個小程序:

#define _ENABLE_ATOMIC_ALIGNMENT_FIX 1
#include <atomic>
#include <iostream>
#include <thread>
#include <assert.h>
#include <array>

using namespace std;
constexpr int numTries = 10000;
constexpr int arraySize = 10000;
array<atomic<int>, arraySize> tat;
atomic<int> tsync {0};

void writeArray()
{
    // Stores atomics in reverse order
    for (int j=0; j!=numTries; ++j)
    {
        for (int i=arraySize-1; i>=0; --i)
        {
            tat[i].store(j, memory_order_relaxed);
        }
        tsync.store(0, memory_order_release);
    }
}

void readArray()
{
    // Loads atomics in normal order
    for (int j=0; j!=numTries; ++j)
    {
        bool readFail = false;
        tsync.load(memory_order_acquire);

        int minValue = 0;
        for (int i=0; i!=arraySize; ++i)
        {
            int newValue = tat[i].load(memory_order_relaxed);
            // If it fails, it stops the execution
            if (newValue < minValue)
            {
                readFail = true;
                cout << "fail " << endl;
                break;
            }
            minValue = newValue;
        }

        if (readFail) break;
    }
}


int main()
{
    for (int i=0; i!=arraySize; ++i)
    {
        tat[i].store(0);
    }

    thread b(readArray);
    thread a(writeArray);

    a.join();
    b.join();
}

它是如何工作的:有一個原子數組。 一個線程以相反的順序以寬松的順序存儲,並以釋放順序存儲一個控制原子。

另一個線程加載控制原子的獲取順序,然后它以輕松的原子加載數組值的 rest。 由於父母不能在孩子之前更新,所以 newValue 應該總是等於或大於 oldValue。

我已經在我的電腦上多次執行了這個程序,調試和發布,它並沒有觸發失敗。 我使用的是普通的 x64 Intel i7 處理器。

那么,假設對多個原子的寬松存儲至少在與控制原子同步並獲取/釋放時確實保持“修改順序”是安全的嗎?

遺憾的是,您將通過 x86_64 的實驗了解標准支持的內容很少,因為 x86_64 的表現非常好。 特別是,除非您指定_seq_cst

  • 所有讀取都有效地_acquire

  • 所有的寫入都是有效的_release

除非它們跨越緩存線邊界。 和:

  • 所有讀-修改-寫都是有效的seq_cst

除了(也)允許編譯器重新排序_relaxed操作。

您提到使用_relaxed fetch_or... 如果我理解正確,您可能會失望地得知它並不比seq_cst 便宜,並且需要LOCK前綴指令,承擔全部開銷。


但是,是的,就排序而言, _relaxed原子操作與普通操作沒有區別。 所以是的,它們可能會被編譯器和/或機器重新排序為其他_relaxed原子操作以及非原子操作。 [盡管如前所述,在 x86_64 上,而不是在機器上。]

並且,是的,在線程 X 中的釋放操作與線程 Y 中的獲取操作同步的情況下,線程 X 中的所有寫入都在釋放之前發生順序 - 在線程 Y 中的獲取之前。所以釋放操作是一個信號X 中所有在它之前的寫入都是“完成的”,並且當獲取操作看到信號 Y 知道它已經同步並且可以讀取 X 寫入的內容(直到發布)。

現在,這里要理解的關鍵是僅僅做一個 store _release是不夠的,存儲的必須是一個明確的信號,告訴 load_acquire存儲已經發生。 否則,負載如何判斷?

通常,像這樣的_release / _acquire對用於同步對某些數據集合的訪問。 一旦該數據“准備就緒”,store _release 就會發出信號。 任何看到信號的加載_acquire (或看到信號的所有加載_acquire )都知道數據“准備好”並且可以讀取它。 當然,在 store _release之后對數據的任何寫入都可能(取決於時間)被 load(s) _acquire 看到 我在這里想說的是,如果要對數據進行進一步更改,可能需要另一個信號。

你的小測試程序:

  1. tsync初始化為 0

  2. 在作家中:畢竟tat[i].store(j, memory_order_relaxed)tsync.store(0, memory_order_release)

    所以tsync的值不會改變!

  3. 在閱讀器中: tsync.load(memory_order_acquire)在執行tat[i].load(memory_order_relaxed) .load(memory_order_relaxed) 之前

    並忽略從tsync讀取的值

我在這里告訴你_release / _acquire沒有同步——所有這些存儲/加載也可能是_relaxed [我認為如果作者設法領先於讀者,您的測試將“通過”。 因為在 x86-64 上,所有寫入都是按指令順序完成的,所有讀取也是如此。]

為了測試_release / _acquire語義,我建議:

  1. tsync初始化為 0 並將tat[]初始化為全零。

  2. 在作者中:運行j = 1..numTries

    畢竟tat[i].store(j, memory_order_relaxed) ,寫tsync.store(j, memory_order_release)

    這表明傳遞已完成,並且所有tat[]現在都是j

  3. 在閱讀器中:做j = tsync.load(memory_order_acquire)

    穿過tat[]應該找到j <= tat[i].load(memory_order_relaxed)

    在 pass 之后, j == numTries表示作者已經完成。

writer 發送的信號是它剛剛完成寫入j ,並將繼續j+1 ,除非j == numTries 但這並不能保證tat[]的寫入順序。

如果您想要的是讓作者在每次通過后停止,並等待讀者看到它並發出相同的信號 - 那么您需要另一個信號並且您需要線程等待它們各自的“您可以繼續”信號。

關於放寬修改順序一致性的引用。 僅表示所有線程都可以就該object 的修改順序達成一致。 即存在訂單。 與另一個線程中的獲取加載同步的稍后發布存儲將保證它是可見的。https://preshing.com/20120913/acquire-and-release-semantics/有一個很好的圖表。

每當您存儲其他線程可以加載和取消引用的指針時,如果最近修改了任何指向的數據,如果讀者也需要看到這些更新,則至少使用mo_release (這包括任何間接可達的東西,比如你的樹的層次。)

在任何類型的樹/鏈表/基於指針的數據結構上,幾乎唯一可以使用輕松的時間是在尚未“發布”到其他線程的新分配節點中。 (理想情況下,您可以只將 args 傳遞給構造函數,這樣它們就可以被初始化,甚至根本不嘗試成為原子; std::atomic<T>()的構造函數本身不是原子的。所以在發布時必須使用發布存儲指向新構造的原子 object 的指針。)


在 x86 / x86-64 上, mo_release沒有額外費用; 普通的 asm 存儲已經具有與發布一樣強的排序,因此編譯器只需要阻止編譯時重新排序即可實現var.store(val, mo_release); 它在 AArch64 上也很便宜,特別是如果你不久之后不做任何獲取負載。

這也意味着您無法使用 x86 硬件測試放松不安全; 編譯器將在編譯時為寬松的存儲選擇一個順序,以它選擇的任何順序將它們固定到發布操作中。 (並且 x86 atomic-RMW 操作始終是完整的障礙,實際上是 seq_cst。在源代碼中使它們更弱僅允許編譯時重新排序。一些非 x86 ISA 可以具有更便宜的 RMW 以及加載或存儲較弱的訂單,但是,即使acq_rel 在 PowerPC 上稍微便宜一些。)

暫無
暫無

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

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