簡體   English   中英

多線程隊列原子操作

[英]Multithread queue atomic operations

我正在玩std :: atomic結構並編寫了這個無鎖的多生產者多用戶隊列,我在這里附上。 隊列的想法基於兩個堆棧 - 生產者和消費者堆棧,它們本質上是鏈接列表結構。 列表的節點將索引保存到一個數組中,該數組包含您要讀取或寫入的實際數據。

想法是列表的節點是互斥的,即,指向節點的指針只能存在於生產者或消費者列表中。 生產者將嘗試從生產者列表中獲取節點,從消費者列表中獲取消費者,並且無論何時生產者或消費者獲取指向節點的指針,它都應該在兩個列表之外,以便其他人無法獲得它。 我正在使用std :: atomic_compare_exchange函數進行旋轉,直到彈出一個節點。

問題是邏輯肯定有問題,或者操作不是原子的,因為我認為它們是因為即使有1個生產者和1個消費者,只要有足夠的時間,隊列就會活鎖,我注意到的是,如果你斷言那個單元!= cell-> m_next,斷言會被擊中! 所以它可能是我臉上的東西,我只是看不到它,所以我想知道是否有人可以投入。

謝謝

#ifndef MTQueue_h
#define MTQueue_h

#include <atomic>

template<typename Data, uint64_t queueSize>
class MTQueue
{
public:

    MTQueue() : m_produceHead(0), m_consumeHead(0)
    {
        for(int i=0; i<queueSize-1; ++i)
        {
            m_nodes[i].m_idx = i;
            m_nodes[i].m_next = &m_nodes[i+1];
        }
        m_nodes[queueSize-1].m_idx = queueSize - 1;
        m_nodes[queueSize-1].m_next = NULL;

        m_produceHead = m_nodes;
        m_consumeHead = NULL;
    }

    struct CellNode
    {
        uint64_t m_idx;
        CellNode* m_next;
    };

    bool push(const Data& data)
    {
        if(m_produceHead == NULL)
            return false;

        // Pop the producer list.
        CellNode* cell = m_produceHead;
        while(!std::atomic_compare_exchange_strong(&m_produceHead,
                                                   &cell, cell->m_next))
        {
            cell = m_produceHead;
            if(!cell)
                return false;
        }

        // At this point cell should point to a node that is not in any of the lists
        m_data[cell->m_idx] = data;

        // Push that node as the new head of the consumer list
        cell->m_next = m_consumeHead;
        while (!std::atomic_compare_exchange_strong(&m_consumeHead,
                                                    &cell->m_next, cell))
        {
            cell->m_next = m_consumeHead;
        }
        return true;
    }

    bool pop(Data& data)
    {
        if(m_consumeHead == NULL)
            return false;

        // Pop the consumer list
        CellNode* cell = m_consumeHead;
        while(!std::atomic_compare_exchange_strong(&m_consumeHead,
                                                   &cell, cell->m_next))
        {
            cell = m_consumeHead;
            if(!cell)
                return false;
        }

        // At this point cell should point to a node that is not in any of the lists
        data = m_data[cell->m_idx];

        // Push that node as the new head of the producer list
        cell->m_next = m_produceHead;
        while(!std::atomic_compare_exchange_strong(&m_produceHead,
                                                   &cell->m_next, cell))
        {
            cell->m_next = m_produceHead;
        }
        return true;
    };

private:

    Data m_data[queueSize];

    // The nodes for the two lists
    CellNode m_nodes[queueSize];

    volatile std::atomic<CellNode*> m_produceHead;
    volatile std::atomic<CellNode*> m_consumeHead;
};

#endif

我相信我能破解這個。 對於從2到1024的隊列以及從1個生產者和1個消費者到100個生產者/ 100個消費者的隊列,沒有1000000的活鎖寫入/讀取。

這是解決方案。 訣竅是不要在compare和swap中直接使用cell-> m_next(順便說一下,這同樣適用於生產者代碼)並要求嚴格的內存順序規則:

這似乎證實了我懷疑是編譯器對讀寫的重新排序。 這是代碼:

bool push(const TData& data)
{
    CellNode* cell = m_produceHead.load(std::memory_order_acquire);
    if(cell == NULL)
        return false;

    while(!std::atomic_compare_exchange_strong_explicit(&m_produceHead,
                                                        &cell,
                                                        cell->m_next,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        if(!cell)
            return false;
    }

    m_data[cell->m_idx] = data;

    CellNode* curHead = m_consumeHead;
    cell->m_next = curHead;
    while (!std::atomic_compare_exchange_strong_explicit(&m_consumeHead,
                                                         &curHead,
                                                         cell,
                                                         std::memory_order_acquire,
                                                         std::memory_order_release))
    {
        cell->m_next = curHead;
    }

    return true;
}

bool pop(TData& data)
{
    CellNode* cell = m_consumeHead.load(std::memory_order_acquire);
    if(cell == NULL)
        return false;

    while(!std::atomic_compare_exchange_strong_explicit(&m_consumeHead,
                                                        &cell,
                                                        cell->m_next,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        if(!cell)
            return false;
    }

    data = m_data[cell->m_idx];

    CellNode* curHead = m_produceHead;
    cell->m_next = curHead;
    while(!std::atomic_compare_exchange_strong_explicit(&m_produceHead,
                                                        &curHead,
                                                        cell,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        cell->m_next = curHead;
    }

    return true;
};

我看到你的隊列實現有一些問題:

  1. 它不是一個隊列,它是一個堆棧:推送的最新項目是彈出的第一個項目。 並不是說堆棧有什么問題,但把它稱為隊列會讓人感到困惑。 實際上它是兩個無鎖堆棧:一個堆棧最初填充節點數組,另一個堆棧使用第一個堆棧作為空閑節點列表存儲實際數據元素。

  2. pushpop CellNode::m_next上存在數據競爭( CellNode::m_next ,因為它們都做同樣的事情,即從一個堆棧彈出一個節點並將該節點推送到另一個堆棧上)。 假設兩個線程同時輸入例如pop並且都從m_consumeHead讀取相同的值。 線程1競爭成功彈出並設置data 然后,線程1將m_produceHead的值寫入cell->m_next而線程2同時讀取cell->m_next以傳遞給std::atomic_compare_exchange_strong_explicit 根據定義,兩個線程同時非原子讀取和寫入cell->m_next是數據競爭。

    這就是並發文獻中所謂的“良性”競賽:讀取陳舊/無效的值,但永遠不會被使用。 如果您確信您的代碼永遠不需要在可能導致火爆的架構上運行,您可能會忽略它,但是為了嚴格遵守標准內存模型,您需要使m_next成為原子並使用至少memory_order_relaxed讀取來消除數據競賽。

  3. ABA。 比較交換循環的正確性基於這樣的前提:在初始加載和后面的比較交換中具有相同值的原子指針(例如, m_produceHeadm_consumeHead )意味着指針對象因此也必須保持不變。 , 這個前提並不適用於任何可以比某些線程通過其比較交換循環更快地回收對象的設計。 考慮這一系列事件:

    1. 線程1進入pop並讀取m_consumeHeadm_consumeHead->m_next的值,但在調用compare-exchange之前阻塞。
    2. 線程2成功地從m_consumeHead彈出該節點並阻塞。
    3. 線程3將幾個節點推送到m_consumeHead
    4. 線程2解除m_produceHead並將原始節點推送到m_produceHead
    5. 線程3從m_produceHead彈出該節點,並將其推回到m_consumeHead
    6. 線程1最終解除阻塞並調用compare-exchange函數,該函數由於m_consumeHead的值相同而成功。 它彈出節點 - 這一切都很好 - 但是將m_consumeHead設置為它在步驟1中讀回的陳舊m_next值。同時線程3推送的所有節點都被泄露。

暫無
暫無

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

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