【问题标题】:C++ Thread safe queue shutdownC++线程安全队列关闭
【发布时间】:2016-09-15 13:36:12
【问题描述】:

我在 C++ 中使用这个类来设置生产者-消费者:

#pragma once

#include <queue>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <atomic>

template <typename T> class SafeQueue
{
public:
    SafeQueue() :
    _shutdown(false)
    {

    }

    void Enqueue(T item)
    {
        std::unique_lock<std::mutex> lock(_queue_mutex);
        bool was_empty = _queue.empty();
        _queue.push(std::move(item));
        lock.unlock();

        if (was_empty)
            _condition_variable.notify_one();
    }

    bool Dequeue(T& item)
    {
        std::unique_lock<std::mutex> lock(_queue_mutex);

        while (!_shutdown && _queue.empty())
            _condition_variable.wait(lock);

        if(!_shutdown)
        {
            item = std::move(_queue.front());
            _queue.pop();

            return true;
        }

        return false;
    }

    bool IsEmpty()
    {
        std::lock_guard<std::mutex> lock(_queue_mutex);
        return _queue.empty();
    }

    void Shutdown()
    {
        _shutdown = true;
        _condition_variable.notify_all();
    }

private:
    std::mutex _queue_mutex;
    std::condition_variable _condition_variable;
    std::queue<T> _queue;
    std::atomic<bool> _shutdown;
};

我是这样使用它的:

class Producer
{
public:
    Producer() :
        _running(true),
        _t(std::bind(&Producer::ProduceThread, this))
    { }

    ~Producer()
    {
        _running = false;
        _incoming_packets.Shutdown();
        _t.join();
    }

    SafeQueue<Packet> _incoming_packets;

private:
    void ProduceThread()
    {
        while(_running)
        {
            Packet p = GetNewPacket();
            _incoming_packets.Enqueue(p);
        }
    }

    std::atomic<bool> _running;
    std::thread _t;
}

class Consumer
{
    Consumer(Producer* producer) :
        _producer(producer),
        _t(std::bind(&Consumer::WorkerThread, this))
    { }

    ~Consumer()
    {
        _t.join();
    }

private:
    void WorkerThread()
    {
        Packet p;

        while(producer->_incoming_packets.Dequeue(p))
            ProcessPacket(p);
    }

    std::thread _t;
    Producer* _producer;
}

大部分都有效。但是偶尔当我删除生产者(并导致它的解构器调用SafeQueue::Shutdown_t.join() 永远阻塞。

我的猜测是问题出在此处(SafeQueue::Dequeue):

while (!_shutdown && _queue.empty())
        _condition_variable.wait(lock);

来自线程#1 的SafeQueue::Shutdown 在线程#2 完成检查_shutdown 时被调用,但在它执行_condition_variable.wait(lock) 之前,它“错过”了notify_all()。这会发生吗?

如果这是问题所在,最好的解决方法是什么?

【问题讨论】:

  • 您是否将警告设置为最高级别?您的 Consumer 类中有一个微妙的错误。您的数据成员的顺序和您打算在构造函数中初始化它们的顺序冲突......检查它。线程将在分配_producer 之前创建
  • 你再次错误地使用了条件变量......它不应该在这样的while循环中。它有一个允许进行此类测试的重载
  • @WhiZTiM 我知道它有这样的重载,但它相当于一个 while 循环:en.cppreference.com/w/cpp/thread/condition_variable/wait
  • @WhiZTiM 我不完全理解这是如何导致活锁的,也许我在这里遗漏了一些明显的东西...... std::condition_variable::wait: @987654333 @(来自en.cppreference.com/w/cpp/thread/condition_variable/wait
  • 这会发生吗?是的。 notify_all 不保证线程上下文切换。消费者可以在等待调用中,~生产者会销毁队列。消费者指向生产者的指针现在无效。队列之前占用的内存无效。现在未定义激活消费者线程时等待代码的行为方式。

标签: c++ multithreading concurrency queue


【解决方案1】:

由于 SafeQueue 对象归生产者所有,因此删除生产者会导致通知消费者和在 ~Producer 完成时从其下删除 SafeQueue 之间的竞争条件。

我建议共享资​​源既不属于生产者也不属于消费者,而是作为参考传递给每个人的构造函数。

更改生产者和消费者构造函数;

Producer( SafeQueue<Packet> & queue ) :
    _running(false), _incoming_packets(queue) {}


Consumer( SafeQueue<Packet> & queue ) :
    _running(false), _incoming_packets(queue) {}

以这种方式使用您的实例;

SafeQueue<Packet> queue;
Producer producer(queue);  
Consumer consumer(queue);

...do stuff...

queue.shutdown();

这也解决了 Consumer 类与 Producer 类紧密耦合的糟糕设计问题。

另外,在析构函数中杀死和加入线程可能是个坏主意,就像为 ~Producer 所做的那样。最好为每个线程类添加一个 Shutdown() 方法,并显式调用它们;

producer.shutdown();
consumer.shutdown();
queue.shutdown();

关闭顺序并不重要,除非您担心在停止消费者时会丢失仍在队列中的未处理数据包。

【讨论】:

  • 为什么在解构器中加入线程是一个糟糕的设计?不是和RAII冲突吗?
  • @UnTraDe,创建和销毁将是 RAII,启动和停止是程序控制。但是,进一步,join 可以抛出异常,这意味着正确处理需要在析构函数中使用更多逻辑,这肯定与 RAII 冲突。
【解决方案2】:

在您的SafeQueue::Dequeue 中,您可能以错误的方式使用std::condition_variable...更改此:

bool Dequeue(T& item)
{
    std::unique_lock<std::mutex> lock(_queue_mutex);

    while (!_shutdown && _queue.empty())
        _condition_variable.wait(lock);
    if(!_shutdown)
    {
        item = std::move(_queue.front());
        _queue.pop();
        return true;
    }
    return false;
}

bool Dequeue(T& item)
{
    std::unique_lock<std::mutex> lock(_queue_mutex);
   _condition_variable.wait(lock, []{ return _shutdown || !_queue.empty() });
    if(!_shutdown)
    {
        item = std::move(_queue.front());
        _queue.pop();
        return true;
    }

    return false;
}

其次,Consumer中数据成员的初始化顺序与其构造函数有关

class Consumer
{
    Consumer(Producer* producer) :
        _producer(producer),
        _t(std::bind(&Consumer::WorkerThread, this))
    { }
    ......
    // _t will be constructed first, regardless of your constructor initializer list
    // Meaning, the thread can even start running using an unintialized _producer
    std::thread _t;      
    Producer* _producer;
}

应该重新排序:

class Consumer
{
    Consumer(Producer* producer) :
        _producer(producer),
        _t(std::bind(&Consumer::WorkerThread, this))
    { }
    ......
    Producer* _producer;
    std::thread _t;      
}

CAB's answer 涵盖了您的问题的另一部分

【讨论】:

  • 原来while (!pred) condvar.wait(lock);的代码是正确的,你的修改是错误的,因为传递谓词时,谓词为真时等待停止,所以应该是_shutdown || !_queue.empty()
猜你喜欢
  • 1970-01-01
  • 2013-02-23
  • 2014-03-13
  • 2023-03-04
  • 1970-01-01
  • 2015-02-16
  • 2012-11-05
  • 2010-11-15
  • 1970-01-01
相关资源
最近更新 更多