簡體   English   中英

C++ - 沒有互斥鎖的條件變量?

[英]C++ - Condition Variable without Mutexes?

問題

我想我誤解了 CV-Mutex 設計模式,因為我正在創建一個似乎不需要互斥鎖的程序,只需要 CV。 如果有人可以幫助解釋我在這里做錯了什么,我很樂意學習。


目標——概述

我正在解析來自 2 個不同帳戶的網站的提要。 AliceBob 解析任務很慢,所以我有兩個單獨的線程,每個線程都專用於處理來自AliceBob的提要。

然后,我有一個線程從網絡接收消息並將工作分配給threadAthreadB ,具體取決於更新消息的對象。 這樣,閱讀器/網絡線程就不會停止, Alice的消息是有序的, Bob的消息也是有序的。

我不在乎Alice線程是否按時間順序落后於Bob線程,只要各個帳戶提要按順序排列即可。


實施細節

這與線程池非常相似,不同之處在於線程基本上被鎖定到大小為 2 的固定大小數組,並且我對每個提要使用相同的線程。

我創建了一個AccountThread class,它維護一個 JSON 消息queue ,以便在 class 中盡快處理。 這是代碼:

#include <queue>
#include <string>
#include <condition_variable>
#include <mutex>

using namespace std;
class AccountThread {

public:
    AccountThread(const string& name) : name(name) { }

    void add_message(const string& d) {
        this->message_queue.push(d);
        this->cv.notify_all(); // could also do notify_one but whatever
    }

    void run_parsing_loop() {
        while (true) {
            std::unique_lock<std::mutex> mlock(lock_mutex);
            cv.wait(mlock, [&] {
                return this->is_dead || this->message_queue.size() > 0;
            });

            if (this->is_dead) { break; }

            const auto message = this->message_queue.front();
            this->message_queue.pop();

            // Do message parsing...
        }
    }

    void kill_thread() {
        this->is_dead = true;
    }


private:
    const string& name;

    condition_variable cv;
    mutex lock_mutex;
    queue<string> message_queue;

    // To Kill Thread if Needed
    bool is_dead;
};

我可以添加 main.cpp 代碼,但它本質上只是一個讀取器循環,它根據帳戶名稱調用thread.add_message(message)


問題

為什么我在這里需要lock_mutex 我看不出它的目的,因為這個 class 本質上是單線程的。 有沒有更好的設計模式呢? 我覺得如果我包含了一個我並不真正需要的變量,比如mutex ,那么我在這個任務中使用了錯誤的設計模式。

我只是在修改我在網上看到的關於線程池實現的一些文章中的代碼,並且很好奇......感謝任何幫助。 謝謝!

首先要做的事情是:沒有互斥鎖就沒有condition_variable::wait wait的接口需要互斥量。 所以關於

我正在創建一個似乎不需要互斥鎖的程序,只需要 CV

請注意,需要互斥鎖來保護條件變量本身。 如果在沒有互斥鎖的情況下如何進行數據競爭的概念沒有立即意義,請檢查為什么 pthreads 的條件變量函數需要互斥鎖

其次,您提供的代碼中有多個痛點。 考慮解決問題的這個版本,我將在下面解釋問題:

class AccountThread {

public:
    AccountThread(const string& name) : name(name) 
    {
        consumer = std::thread(&AccountThread::run_parsing_loop, this); // 1
    }
    
    ~AccountThread()
    {
        kill_thread(); // 2
        consumer.join();
    }

    void add_message(const string& d) {
        {
            std::lock_guard lok(lock_mutex); // 3
            this->message_queue.push(d);
        }
        this->cv.notify_one();
    }

private:
    void run_parsing_loop() 
    {
        while (!is_dead) {
            std::unique_lock<std::mutex> mlock(lock_mutex);
            cv.wait(mlock, [this] { // 4
                return is_dead || !message_queue.empty();
            });

            if (this->is_dead) { break; }

            std::string message = this->message_queue.front();
            this->message_queue.pop();

            string parsingMsg = name + " is processing " + message + "\n";
            std::cout << parsingMsg;
        }
    }

    void kill_thread() {
        {
            std::lock_guard lock(lock_mutex);
            this->is_dead = true;
        }
        cv.notify_one(); // 5
    }

private:
    string name; // 6

    mutable condition_variable cv; // 7
    mutable mutex lock_mutex;
    std::thread consumer;
    queue<string> message_queue;

    bool is_dead{false}; // 8
};

從上到下指出的問題(在編號的評論中是):

  1. 如果您有一個工作線程 class,例如AccountThread ,那么當 class 提供線程時,更容易正確處理。 這樣,只有相關的接口被暴露,你可以更好地控制消費者的生命周期和工作。
  2. 例如,當AccountThread “死亡”時,工人也應該死亡。 在上面的示例中,我通過終止析構函數中的消費者線程來修復此依賴關系。
  3. add_message在您的代碼中導致了數據競爭 由於您打算在不同的線程中運行解析循環,因此在沒有關鍵部分的情況下簡單地推送到隊列是錯誤的。
  4. this捕獲它會更干凈,例如,您可能不需要對捕獲的mlock的引用。
  5. kill_thread不正確。 您需要通知可能正在等待的消費者線程 state 發生了變化。 要正確執行此操作,您需要使用鎖保護在謂詞中簽入的 state。
  6. 帶有const string &name的初始版本可能不是您想要的。 成員 const 引用不會延長臨時對象的生命周期,並且構造函數的編寫方式可能會使實例帶有懸空的 state。 即使您進行典型檢查,使用 r 值參考版本重載構造函數,您也將依賴於比您的AccountThread object 存活時間更長的外部字符串。 更好地使用價值成員。
  7. 記住M&M 規則
  8. 你有未定義的行為 is_alive成員在未初始化的情況下被使用。

演示

總而言之,我認為建議的改變指向了正確的方向。 如果您想更深入地了解您提到的 TBB 組件之類的實現方式,還可以檢查類似 Go 的通信渠道的實現。 這樣的通道(或緩沖區隊列)將簡化實現以避免手動使用互斥鎖、CV 和活動狀態:

class AccountThread {
public:
    AccountThread(const string& name) : name(name) {
        consumer = std::thread(&AccountThread::run_parsing_loop, this);
    }
    
    ~AccountThread() {
        kill_thread();
        consumer.join();
    }

    void add_message(const string& d) { _data.push(d); }

private:
    void run_parsing_loop() {
        try {
            while (true) {
                // This pop waits until there's data or the channel is closed.
                auto message = _data.pop();
                // TODO: Implement parsing here
            }
        } catch (...) { 
            // Single exception thrown per thread lifetime
        }
    }

    void kill_thread() { _data.set(yap::BufferBehavior::Closed); }

private:
    string name;
    std::thread consumer;
    yap::BufferQueue<string> _data;
};

演示2

暫無
暫無

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

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