![](/img/trans.png)
[英]How to make push and pop on queue atomic, how to lock those operations?
[英]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;
};
我看到你的隊列實現有一些問題:
它不是一個隊列,它是一個堆棧:推送的最新項目是彈出的第一個項目。 並不是說堆棧有什么問題,但把它稱為隊列會讓人感到困惑。 實際上它是兩個無鎖堆棧:一個堆棧最初填充節點數組,另一個堆棧使用第一個堆棧作為空閑節點列表存儲實際數據元素。
push
和pop
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
讀取來消除數據競賽。
ABA。 比較交換循環的正確性基於這樣的前提:在初始加載和后面的比較交換中具有相同值的原子指針(例如, m_produceHead
和m_consumeHead
)意味着指針對象因此也必須保持不變。 , 這個前提並不適用於任何可以比某些線程通過其比較交換循環更快地回收對象的設計。 考慮這一系列事件:
pop
並讀取m_consumeHead
和m_consumeHead->m_next
的值,但在調用compare-exchange之前阻塞。 m_consumeHead
彈出該節點並阻塞。 m_consumeHead
。 m_produceHead
並將原始節點推送到m_produceHead
。 m_produceHead
彈出該節點,並將其推回到m_consumeHead
。 m_consumeHead
的值相同而成功。 它彈出節點 - 這一切都很好 - 但是將m_consumeHead
設置為它在步驟1中讀回的陳舊m_next
值。同時線程3推送的所有節點都被泄露。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.