[英]Lock-free stack - Is this a correct usage of c++11 relaxed atomics? Can it be proven?
我為一個非常簡單的數據寫了一個容器,需要跨線程同步。 我想要最好的表現。 我不想使用鎖。
我想用“放松”的原子。 部分是為了一點額外的魅力,部分是為了真正理解它們。
我一直在研究這個問題,而且我正處於這個代碼通過我拋出的所有測試的地步。 但這並不是“證據”,所以我想知道是否有任何我遺漏的東西,或者我可以測試的其他任何方式?
這是我的前提:
這就是我在想的。 “通常”,我們對我們正在閱讀的代碼進行推理的方式是查看它所編寫的順序。 內存可以被讀取或寫入“亂序”,但不能使程序的正確性無效。
這在多線程環境中發生了變化。 這就是內存柵欄的用途 - 這樣我們仍然可以查看代碼並能夠推斷它是如何工作的。
所以,如果一切都可以在這里完全失序,那么我在放松原子能做什么呢? 這不是有點太遠嗎?
我不這么認為,但這就是我在這里尋求幫助的原因。
compare_exchange操作本身可以保證彼此之間具有連續的恆定性。
讀取或寫入原子的唯一另一個時間是在compare_exchange之前獲取頭部的初始值。 它被設置為變量初始化的一部分。 據我所知,這個操作是否帶回了“適當的”值是無關緊要的。
當前代碼:
struct node
{
node *n_;
#if PROCESSOR_BITS == 64
inline constexpr node() : n_{ nullptr } { }
inline constexpr node(node* n) : n_{ n } { }
inline void tag(const stack_tag_t t) { reinterpret_cast<stack_tag_t*>(this)[3] = t; }
inline stack_tag_t read_tag() { return reinterpret_cast<stack_tag_t*>(this)[3]; }
inline void clear_pointer() { tag(0); }
#elif PROCESSOR_BITS == 32
stack_tag_t t_;
inline constexpr node() : n_{ nullptr }, t_{ 0 } { }
inline constexpr node(node* n) : n_{ n }, t_{ 0 } { }
inline void tag(const stack_tag_t t) { t_ = t; }
inline stack_tag_t read_tag() { return t_; }
inline void clear_pointer() { }
#endif
inline void set(node* n, const stack_tag_t t) { n_ = n; tag(t); }
};
using std::memory_order_relaxed;
class stack
{
public:
constexpr stack() : head_{}{}
void push(node* n)
{
node next{n}, head{head_.load(memory_order_relaxed)};
do
{
n->n_ = head.n_;
next.tag(head.read_tag() + 1);
} while (!head_.compare_exchange_weak(head, next, memory_order_relaxed, memory_order_relaxed));
}
bool pop(node*& n)
{
node clean, next, head{head_.load(memory_order_relaxed)};
do
{
clean.set(head.n_, 0);
if (!clean.n_)
return false;
next.set(clean.n_->n_, head.read_tag() + 1);
} while (!head_.compare_exchange_weak(head, next, memory_order_relaxed, memory_order_relaxed));
n = clean.n_;
return true;
}
protected:
std::atomic<node> head_;
};
與其他人相比,這個問題有什么不同? 放松的原子。 他們對這個問題產生了很大的影響。
所以你怎么看? 有什么我想念的嗎?
push
已損壞,因為在compareAndSwap
失敗后你不會更新node->_next
。 當下一次compareAndSwap
嘗試成功時,最初使用node->setNext
存儲的node->setNext
已被另一個線程從堆棧頂部彈出。 其結果是,一些線程認為它已經從堆棧中彈出一個節點,但這個線程已經把它放回堆棧。 它應該是:
void push(Node* node) noexcept
{
Node* n = _head.next();
do {
node->setNext(n);
} while (!_head.compareAndSwap(n, node));
}
此外,由於next
和setNext
使用memory_order_relaxed
,因此無法保證_head_.next()
此處返回最近推送的節點。 可以從堆棧頂部泄漏節點。 pop
中也存在同樣的問題: _head.next()
可能會返回一個先前但不再位於堆棧頂部的節點。 如果返回的值為nullptr
,則當堆棧實際上不為空時,可能無法彈出。
如果兩個線程同時嘗試從堆棧中彈出最后一個節點,則pop
也可能具有未定義的行為。 它們都看到_head.next()
的相同值,一個線程成功完成pop。 另一個線程進入while循環 - 因為觀察到的節點指針不是nullptr
- 但是compareAndSwap
循環很快將它更新為nullptr
因為堆棧現在是空的。 在循環的下一次迭代中,該nullptr被_next
以獲得其_next
指針並且隨之而來的是非常歡鬧。
pop
也明顯患有ABA。 兩個線程可以在堆棧頂部看到相同的節點。 假設一個線程到達評估_next
指針然后阻塞的程度。 另一個線程成功彈出節點,推送5個新節點,然后在另一個線程喚醒之前再次推送該原始節點。 其他線程的compareAndSwap
將成功 - 棧頂節點是相同的 - 但將舊的_next
值存儲到_head
而不是新的。 另一個線程推送的五個節點都被泄露了。 這也是memory_order_seq_cst
的情況。
讓一方面難以實現pop操作,我認為memory_order_relaxed
是不合適的。 在推送節點之前,假設將向其寫入一些值,當彈出節點時將讀取該值。 您需要一些同步機制來確保在讀取值之前實際寫入了值。 memory_order_relaxed
沒有提供同步... memory_order_acquire
/ memory_order_release
會。
這段代碼完全被破壞了。
這看起來有效的唯一原因是當前編譯器對原子操作的重新排序不是很積極,x86處理器有很強的保證。
第一個問題是沒有同步,不能保證該數據結構的客戶端甚至會看到要初始化的節點對象的字段。 下一個問題是,如果沒有同步,推送操作可以讀取頭部標簽的任意舊值。
我們開發了一個工具CDSChecker,它模擬了內存模型允許的大多數行為。 它是開源和免費的。 在您的數據結構上運行它以查看一些有趣的執行。
在這一點上,證明利用輕松原子的代碼是一個很大的挑戰。 大多數證明方法都會被破壞,因為它們通常具有歸納性,並且您沒有訂單可以導入。 所以你可以憑空閱讀問題......
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.