资源有限的生产者 - 工人 - 消费者

时间:2014-08-23 21:01:00

标签: c++ multithreading c++11

我想通过资源有限的中间“工人”线程对通常的生产者 - 消费者问题进行略微修改。示例应用程序可以是:

  • 生产者线程从文件中读取记录并将它们放入队列中。一旦到达文件末尾,就应该向工作线程发送通知。
  • 一个或多个'worker'线程从生产者队列中提取记录,进行某种处理,并将处理过的记录推送到另一个队列。处理完所有记录后,会向消费者线程发送通知。
  • 单个使用者线程将处理过的记录写入文件。

我不是说这是解决此类问题的好方法,但它突出了我正在努力解决的问题,即如何正确地通知工作者和消费者线程。

我有一个带有以下界面的线程安全队列:

template<class T>
class threadsafe_queue
{
public:
    threadsafe_queue() {}
    threadsafe_queue(const threadsafe_queue& other);
    void push(T new_value);
    void wait_and_pop(T& value);
    std::shared_ptr<T> wait_and_pop();
    bool try_pop(T& value);
    std::shared_ptr<T> try_pop();
    bool empty() const;
};

我用单个工作线程解决问题的第一个想法是使用两个原子bool,如下所示:

#include <chrono>
#include <thread>

void queue_producer(threadsafe_queue<unsigned>& queue, std::atomic<bool>& producer_finished)
{
    for (unsigned i = 0; i < 10; ++i) {
        queue.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    producer_finished.store(true);
    std::cout << "Producer finished." << std::endl;
}

void queue_processor(threadsafe_queue<unsigned>& in_queue, threadsafe_queue<unsigned>& out_queue,
                 std::atomic<bool>& producer_finished, std::atomic<bool>& processor_finished)
{
    unsigned value;
    while (!producer_finished.load()) {
        in_queue.wait_and_pop(value);
        value *= 10;
        out_queue.push(value);
    }
    processor_finished.store(true);
    std::cout << "Processor finished." << std::endl;
}

void queue_consumer(threadsafe_queue<unsigned>& queue, std::atomic<bool>& processor_finished)
{
    unsigned value;
    while (!processor_finished.load()) {
        queue.wait_and_pop(value);
        std::cout << "Received value " << value << "." << std::endl; // Or write to file etc.
    }
    std::cout << "Consumer finished." << std::endl;
}

int main(int argc, const char * argv[])
{
    std::atomic<bool> producer_finished(false);
    std::atomic<bool> processor_finished(false);

    threadsafe_queue<unsigned> in_queue;
    threadsafe_queue<unsigned> out_queue;

    std::thread producer_thread(queue_producer, std::ref(in_queue), std::ref(producer_finished));
    std::thread processor_thread(queue_processor, std::ref(in_queue), std::ref(out_queue), std::ref(producer_finished), std::ref(processor_finished));
    std::thread consumer_thread(queue_consumer, std::ref(out_queue), std::ref(processor_finished));

    producer_thread.join();
    processor_thread.join();
    consumer_thread.join();

    return 0;
}

这个问题是处理器(和消费者)可以在设置原子bool之前重新进入while循环,因此无限期地等待永远不会出现的记录。

我还认为解决方案可能是将某种类型的sentinel值推送到队列以表示结束(可以使用包装类),但这似乎并不是一种特别好的方法,并且它不适用于多工作者版本。我实际上认为多工作者版本是一个更加困难的问题,因此对单一工作者版本的任何帮助都是一个很好的开始。

2 个答案:

答案 0 :(得分:0)

您可以将<condition_variable>与通知原则一起考虑<mutex>

你需要某个地方:

std::mutex mtx;                     // for locking  
std::condition_variable ready;      // for waiting conditions 

在生产者方面,您处理输入并保护队列的更新:

{
        std::lock_guard<std::mutex> guard(mtx);     // lock
        // ... updated the queue
        ready.notify_all();                 // notify consumers
}                                           // the lock is removed here 

在消费者方面,你有一个循环:

{
    std::unique_lock<std::mutex> guard(mtx);        // lock 
                                                    // wait condition
    ready.wait(guard, [&]() {return /* queue not empty */; });  // lock is released as long as condition is false 
    //... here lock is set and condition is true: read element from queue
    //... if processing is long release the lock when queue is read
}    // otherwhise the lock is removed here

您可以找到包含条件变量生产者/消费者示例here的教程。请注意notify_one()notify_all();我目前正在尝试它,据我所知,第一个是生产者/非专业化消费者的最佳选择。如果每个消费者都是专业的,那么后者是适当的,即必须知道他是否能够处理输入。

其他方法

使用您当前的代码。我得到的线条&#34;收到&#34;显示从10到90,显示所有线程已贡献,但程序挂起。

如果我用this_thread::yield()替换等待以避免不必要的等待,所有三个线程立即声明他们已经完成了他们的工作。为什么?因为生产者运行得更快,当处理器有机会工作时,producer_finished是真的。然后处理器甚至不运行循环。

因此,您的代码严重依赖于执行顺序。请记住,您的队列是线程安全的,您可以通过修改处理器和使用者的while条件来改善情况,如下所示:

while (!producer_finished.load() || !in_queue.empty()) {
...
while (!processor_finished.load() || !queue.empty()) {
...

来自上述条件变量方法的公寓,另一种方法可能是使用std::promise<T>(例如:处理器发送通知)和std::shared_future<T>(例如:让消费者知道他有什么可以处理的)

答案 1 :(得分:0)

邮箱 - 解决生产者/消费者问题

您可以使用名为Message Box(也称为Mailbox)的其中一个线程同步基元将Producer和Consumer线程链接在一起。可以使用C ++ 11 Mutex和Condition Variable实现邮箱。

邮箱提供了两个线程交换信息的方法。通常,一个线程将生成消息并发送到另一个线程进行处理。邮件包含在邮箱中,因此线程安全。

<强>生产者

mbox.Put(message);   

尝试在指定的邮箱中放置邮件。如果邮箱已满,则呼叫可能会阻止或不依赖于设计。

<强>消费

message = mbox.Get();   // Blocking

在指定邮箱可用时删除邮件,并返回邮件的地址或移动副本。

Put和Get方法的代码段:

/// <summary>Push an item to the back of the queue. Move.</summary>
void Put(T && itemToAddByMoving)
{
    {
        std::lock_guard<std::mutex> lg(m_queueMutex);
        m_deque.push_back(std::forward<T>(itemToAddByMoving));   // Perfect forwarding: rvalue if argument is rvalue
    }   // release lock
    m_queueCondVar.notify_one();    // Notify consumer thread that there is data available in the queue.
}

/// <summary>Waiting for the queue to have data ready (not empty). Pop the first element when ready.</summary>
/// <returns>First element in front of the queue.</returns>
T Get()
{
    T poppedValue;      // T should support move.
    {
        std::unique_lock<std::mutex> ul(m_queueMutex);
        m_queueCondVar.wait(ul, [&]{ return !m_deque.empty(); });   // Wait here. Blocking.
        poppedValue = m_deque.front();
        m_deque.pop_front();
    }   // release lock
    return poppedValue;
}