簡體   English   中英

c++ 無鎖隊列實現單生產者單消費者

[英]c++ lock free queue implementation single producer single consumer

我嘗試了一個無鎖的單一生產者-單一消費者實現鏈接 該實現使用列表作為底層數據結構,並依賴於只有生產者線程修改列表這一事實。如果 _head 不等於 _tail,則消費者線程移動 head 變量。

int produced_count_, consumed_count_;
std::list<int> data_queue_;
std::list<int>::iterator head_, tail_;    

void ProducerConumer::produce() {
    static int count = 0;
    data_queue_.push_back(int(count++));
    ++produced_count_;
    tail_ = data_queue_.end();
    data_queue_.erase(data_queue_.begin(), head_);
}

bool ProducerConumer::consume() {
    auto it = head_;
    ++it;
    if(it != tail_) {
        head_ = it;
        ++consumed_count_;
        int t = *it;
        return true;
    } 
    
    return false;
}

在任何時候,頭迭代器都指向一個已被讀取的值。

由於這里沒有同步,我的印象是該實現不會工作,因為一個線程的寫入可能對另一個線程不可見。 但是當我測試我的代碼時,生產者和消費者總是生產/消費相同數量的單位。 有人可以解釋這段代碼如何在沒有顯式同步的情況下工作嗎? (我沒想到對其他線程可見的 tail_ 和 head_ 變量的更改)

控制生產者/消費者線程的代碼如下

consumer_thread_ = std::thread([this]() {
set_cpu_affinity(0);
std::chrono::milliseconds start_time = current_time();
while((current_time() - start_time) < std::chrono::milliseconds(150)) {
        this->consume();
    }
    std::cout << "Data queue size from consumer is " << data_queue_.size() << " time " << current_time().count() << "\n";
});

producer_thread_ = std::thread([this]() {
    set_cpu_affinity(7);
    std::chrono::milliseconds start_time = current_time();
    while((current_time() - start_time) < std::chrono::milliseconds(100)) {
        this->produce();
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Data queue size from producer is " << data_queue_.size() << " time " << current_time().count() << "\n";
});

我通過在生產者線程末尾添加 sleep_for 來確保生產者線程比消費者線程長壽。

順便說一句,這里是 Herb Sutter 討論它鏈接有什么問題的實現的剖析。 但是他從來沒有談到對 tail_ 和 head_ 的更改是否對其他線程可見。

調試版本通常會“碰巧工作”,尤其是在 x86 上,因為對代碼生成塊編譯時重新排序施加的約束,而 x86 硬件阻止大多數運行時重新排序。

如果在調試模式下編譯,memory 次訪問將按程序順序發生,並且編譯器不會跨語句將值保存在寄存器中 (有點像 volatile,它可以用來滾動你自己的原子;但不要: 何時將 volatile 與多線程一起使用? )。 仍然,緩存是連貫的,簡單的加載和存儲是 asm 就足以實現全局可見性(以某種順序)。

它們將是原子的,因為它們是int大小和對齊的,編譯器用一條指令完成它們,因為它不是 DeathStation 9000。自然對齊的int加載和存儲在普通機器上的 asm 中是原子的,如 x86 ,在 C 中不能保證. ( https://lwn.net/Articles/793253/ )

如果您僅在 x86 上進行測試,則硬件 memory model 會為您提供程序順序和存儲緩沖區,因此您可以有效地獲得與std::atomic memory_order_acquirerelease相同的 asm。 (因為調試構建不會在語句之間重新排序)。

C++ 未定義的行為(包括此數據爭用 UB)並不意味着“保證會失敗或崩潰”——這就是它如此令人討厭的原因,也是測試不足以找到它的原因。

在啟用優化的情況下進行編譯,您可能會遇到大問題,具體取決於編譯時重新排序和提升選擇。 例如,如果編譯器可以在循環期間將變量保存在寄存器中,它將永遠不會從緩存/內存中重新讀取,也永遠不會看到其他線程存儲的內容。 除其他問題外。 多線程程序卡在優化模式但在-O0下正常運行

如果代碼只是碰巧在“訓練輪”模式下工作,那么它就不是很有用,因為您沒有告訴編譯器如何安全地優化它。 (例如,通過使用std::atomic )。


我沒有詳細查看您的代碼,但我認為您沒有任何變量被兩個線程修改 在循環緩沖區隊列中,您通常會在生產者 RMW 但消費者只讀的變量上有一個++增量。 反之亦然,讀取 position。這些不需要是原子 RMW,只需是原子存儲,以便其他線程的原子加載可以看到未撕裂的值。 這發生在 asm 中的“自然”。

在這里我認為你只是在存儲一個新的頭部,而另一個線程只是在讀取它。

在鏈表中,釋放可能是一個問題,尤其是對於多個消費者。 在確定沒有線程有指向它的指針之前,您不能釋放或回收該節點。 垃圾收集語言/運行時可以更輕松地使用無鎖隊列的鏈表,因為 GC 已經必須處理一般對象的相同檢查。

因此,如果您自己動手,請確保您做對了; 這可能很棘手。 雖然只要你只在一個節點構建后將它鏈接到鏈表中,並且只有一個消費者,你永遠不會看到半構建的節點。 而且您永遠不會讓一個線程取消分配另一個線程可能喚醒並繼續讀取的節點。

文章說:

另一個問題是使用標准的 std::list。 雖然文章提到檢查讀/寫 std::list::iterator 是原子的是開發人員的責任,但事實證明這過於嚴格。 gcc/MSVC++2003 有 4 字節迭代器,而 MSVC++2005 在發布模式下有 8 字節迭代器,在調試模式下有 12 字節迭代器。

你有責任確保迭代器是原子的。 std::list不是這種情況。 除非您將數據明確指定為原子數據,否則無法保證來自不同線程的讀/寫操作。 然而,即使“未定義的行為”意味着“鼻惡魔”,如果這些惡魔被觀察為一致的同步也沒有錯。

暫無
暫無

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

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