繁体   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