簡體   English   中英

使用atomic鎖定自由單個生成器多個使用者數據結構

[英]lock free single producer multiple consumer data struct using atomic

我最近有類似下面的示例代碼(實際代碼要復雜得多)。 看過Hans Boehm關於原子的cppcon16談話后,我有點擔心我的代碼是否有效。

produce由單個生產者線程調用,而consume由多個消費者線程調用。 生產者只更新序列號中的數據,如2,4,6,8,...,但在更新數據之前設置為奇數序列號,如1,3,5,7 ......,以指示數據可能是臟的。 消費者也試圖以相同的順序(2,4,6,...)獲取數據。

消費者在讀取后仔細檢查序列號以確保數據良好(在讀取期間不由生產者更新)。

我認為我的代碼在x86_64(我的目標平台)上運行正常,因為x86_64不會對其他商店重新排序商店,或者加載商店或加載,但我懷疑它在其他平台上是錯誤的。

我是否正確,數據分配(在產品中)可以移動到'store(n-1)'以上,以便消費者讀取損壞的數據,但t == t2仍然成功?

struct S 
{
    atomic<int64_t> seq;
    // data members of primitive type int, double etc    
    ...
};

S s;

void produce(int64_t n, ...) // ... for above data members
{
    s.seq.store(n-1, std::memory_order_release); // indicates it's working on data members

    // assign data members of s
    ...

    s.seq.store(n, std::memory_order_release); // complete updating
}

bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    auto t = s.load(std::memory_order_acquire);

    if (t == n)
    {
        // read fields
        ...

        auto t2 = s.load(std::memory_order_acquire);
        if (t == t2)
            return true;
    }        

    return false;
}

編譯時重新排序仍然會在定位x86時咬你,因為編譯器會優化以保留程序在C ++抽象機器上的行為,而不是任何更強大的依賴於體系結構的行為。 由於我們要避免使用memory_order_seq_cst ,因此允許重新排序。

是的,您的商店可以按照您的建議重新排序。 您的負載也可以使用t2負載重新排序,因為獲取負載只是一個單向障礙 編譯器完全優化t2檢查是合法的。 如果可以重新排序,則允許編譯器確定它始終發生的情況並應用as-if規則來生成更高效的代碼。 (目前的編譯器通常沒有,但是現在的標准絕對允許這樣做。請參閱有關此問題的討論結論,並提供標准提案的鏈接 。)

您防止重新排序的選項是:

  • 使所有數據成員存儲/加載原子化並釋放並獲取語義。 (最后一個數據成員的獲取負載將使t2負載首先完成。)
  • 使用障礙(也稱為柵欄)將所有非原子存儲和非原子載荷作為一個組進行排序。

    正如Jeff Preshing所解釋的那樣, mo_release fencemo_release store不同 ,它是我們需要的雙向屏障。 std :: atomic只是回收std :: mo_名稱而不是為圍欄指定不同的名稱。

    (順便說一句,非原子存儲/加載應該是mo_relaxed原子,因為技術上未定義的行為在它們可能正在被重寫的過程中完全讀取它們,即使你決定不看你讀的內容。 )


void produce(int64_t n, ...) // ... for above data members
{
    /*********** changed lines ************/
    std::atomic_signal_fence(std::memory_order_release);  // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid).
    s.seq.store(n-1, std::memory_order_relaxed);          // changed from release
    std::atomic_thread_fence(std::memory_order_release);  // StoreStore barrier prevents reordering of the above store with any below stores.  (It's also a LoadStore barrier)
    /*********** end of changes ***********/

    // assign data members of s
    ...

    // release semantics prevent any preceding stores from being delayed past here
    s.seq.store(n, std::memory_order_release); // complete updating
}



bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    if (n == s.seq.load(std::memory_order_acquire))
    {
        // acquire semantics prevent any reordering with following loads

        // read fields
        ...

    /*********** changed lines ************/
        std::atomic_thread_fence(std::memory_order_acquire);  // LoadLoad barrier (and LoadStore)
        auto t2 = s.seq.load(std::memory_order_relaxed);    // relaxed: it's ordered by the fence and doesn't need anything extra
        // std::atomic_signal_fence(std::memory_order_acquire);  // compiler barrier: probably not useful on the load side.
    /*********** end of changes ***********/
        if (n == t2)
            return true;
    }

    return false;
}

注意額外的編譯器屏障(signal_fence僅影響編譯時重新排序)以確保編譯器不會將一次迭代中的第二個存儲與下一次迭代中的第一個存儲合並(如果這是在循環中運行)。 或者更一般地,確保盡可能晚地完成使區域無效的商店,以減少誤報。 (可能沒有必要使用真正的編譯器,並且在調用此函數之間有大量代碼。但是signal_fence從不編譯任何指令,並且似乎比將第一個存儲保存為mo_release更好的選擇。在發布存儲線程的架構上 - 編譯到額外的指令,輕松的存儲避免有兩個單獨的屏障指令。)

我還擔心第一個商店可能會重新排序上一次迭代中的發布商店。 但我不認為這種情況會發生,因為兩家商店的地址相同。 (在編譯時,也許標准允許惡意編譯器執行此操作,但任何理智的編譯器都會根本不執行其中一個存儲,如果它認為可以傳遞另一個存儲庫。)在運行時弱的情況下 - 有序的架構,我不確定同一地址的商店是否可能無序地全局可見。 這應該不是現實生活中的問題,因為生產者可能不會背靠背地召喚。


順便說一句, 你正在使用的同步技術是Seqlock ,但只有一個單一編寫器 您只有序列部分,而不是鎖定部分來同步單獨的編寫器。 在多寫程序版本中,編寫者會在讀取/寫入序列號和數據之前獲取鎖定。 (而不是將seq no作為函數arg,你從鎖中讀取它)。

C ++標准 - 討論文件N4455 (關於原子的編譯器優化,請參閱我對Can num ++的答案的后半部分是'int num'的原子? )以它為例。

而不是StoreStore圍欄,他們使用發布商店作為編寫器中的數據項。 (對於原子數據項,正如我所提到的,這需要真正正確)。

void writer(T d1, T d2) {
  unsigned seq0 = seq.load(std::memory_order_relaxed);  // note that they read the current value because it's presumably a multiple-writers implementation.
  seq.store(seq0 + 1, std::memory_order_relaxed);
  data1.store(d1, std::memory_order_release);
  data2.store(d2, std::memory_order_release);
  seq.store(seq0 + 2, std::memory_order_release);
}

他們討論讓讀者第二次加載序列號可能會在以后的操作中重新排序,如果編譯器這樣做是有利的,並且在讀者中使用t2 = seq.fetch_add(0, std::memory_order_release)作為一種潛在的方式獲取具有發布語義的加載。 對於當前的編譯器,我建議這樣做; 你可能會在x86上得到一個lock操作,我上面提到的方式沒有任何(或任何實際的屏障指令,因為只有全屏障seq_cst柵欄需要x86上的指令)。

暫無
暫無

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

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