【问题标题】:Why is std::queue not thread-safe?为什么 std::queue 不是线程安全的?
【发布时间】:2016-08-14 05:17:53
【问题描述】:

主题说了算。我不明白为什么 std::queue(或一般来说:任何队列)本质上不是线程安全的,因为没有像其他数据结构那样涉及迭代器。

根据一般规则

  • 至少有一个线程正在写入...
  • 另一个线程正在从共享资源中读取数据

我应该在以下示例代码中遇到冲突:

#include "stdafx.h"
#include <queue>
#include <thread>
#include <iostream>

struct response
{
    static int & getCount()
    {
        static int theCount = 0;
        return theCount;
    }

    int id;
};


std::queue<response> queue;

// generate 100 response objects and push them into the queue
void produce()
{
    for (int i = 0; i < 100; i++)
    {
        response r; 
        r.id = response::getCount()++;
        queue.push(r);
        std::cout << "produced: " << r.id << std::endl;
    }
}

// get the 100 first responses from the queue
void consume()
{
    int consumedCounter = 0;
    for (;;)
    {       
        if (!queue.empty())
        {
            std::cout << "consumed: " << queue.front().id << std::endl;
            queue.pop();
            consumedCounter++;
        }

        if (consumedCounter == 100)
            break;
    }
}

int _tmain(int argc, _TCHAR* argv[])
{

    std::thread t1(produce);
    std::thread t2(consume);

    t1.join();
    t2.join();

    return 0;
}

一切似乎都运行良好: - 没有违反完整性/数据损坏 - 消费者获取它们的元素的顺序是正确的(0

【问题讨论】:

  • 简单示例:致电q.empty()。如果其他东西可以同时从队列中添加或删除元素,结果意味着什么?该界面不是为并发使用而设计的。
  • 线程安全通常会产生非常大的开销,在没有线程的情况下完全未使用。另外,作为Java found with synchronizing vector,大多数时候需要保护的是事务,而不是个人访问。
  • 一方面,即使std::queue() 就其方法而言是线程安全的,它怎么可能防止在调用front() 和@987654327 之间将项目添加到队列中@?如果您运行程序足够长的时间,您至少应该在输出中看到缺少的 ID,因为在打印一个元素的 ID 时,生产者线程添加了另一个元素,然后在打印它的 ID 之前弹出了一个元素(您会看到刚刚打印的 ID 的副本,因为它不会在适当的时间弹出)。
  • 如果一切看似正常的事情实际上都做了,我们就不需要定期更新软件了。
  • @Stefano:应该注意的是,该文章中的技术依赖于 MSVC++ 如何处理volatile 变量的非标准细节,即使使用 MSVC++ 也可能取决于目标和/或正在使用的编译器选项:msdn.microsoft.com/en-us/library/12a04hfd.aspx

标签: c++ multithreading queue


【解决方案1】:

假设您检查!queue.empty(),进入下一个块,在访问queue.first() 之前,另一个线程将删除(弹出)唯一的元素,因此您查询一个空队列。

使用如下同步队列

#pragma once

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

    template <typename T>
    class SharedQueue
    {
    public:
        SharedQueue();
        ~SharedQueue();

        T& front();
        void pop_front();

        void push_back(const T& item);
        void push_back(T&& item);

        int size();
        bool empty();

    private:
        std::deque<T> queue_;
        std::mutex mutex_;
        std::condition_variable cond_;
    }; 

    template <typename T>
    SharedQueue<T>::SharedQueue(){}

    template <typename T>
    SharedQueue<T>::~SharedQueue(){}

    template <typename T>
    T& SharedQueue<T>::front()
    {
        std::unique_lock<std::mutex> mlock(mutex_);
        while (queue_.empty())
        {
            cond_.wait(mlock);
        }
        return queue_.front();
    }

    template <typename T>
    void SharedQueue<T>::pop_front()
    {
        std::unique_lock<std::mutex> mlock(mutex_);
        while (queue_.empty())
        {
            cond_.wait(mlock);
        }
        queue_.pop_front();
    }     

    template <typename T>
    void SharedQueue<T>::push_back(const T& item)
    {
        std::unique_lock<std::mutex> mlock(mutex_);
        queue_.push_back(item);
        mlock.unlock();     // unlock before notificiation to minimize mutex con
        cond_.notify_one(); // notify one waiting thread

    }

    template <typename T>
    void SharedQueue<T>::push_back(T&& item)
    {
        std::unique_lock<std::mutex> mlock(mutex_);
        queue_.push_back(std::move(item));
        mlock.unlock();     // unlock before notificiation to minimize mutex con
        cond_.notify_one(); // notify one waiting thread

    }

    template <typename T>
    int SharedQueue<T>::size()
    {
        std::unique_lock<std::mutex> mlock(mutex_);
        int size = queue_.size();
        mlock.unlock();
        return size;
    }

对front() 的调用会一直等待,直到它有一个元素并锁定底层队列,因此一次只能有一个线程访问它。

【讨论】:

  • 我喜欢你的实现,我想我会这样做。但是在您的解释中,您假设有多个消费者。他只有一个消费者,所以没有其他人会打电话给空,先或流行。在最坏的情况下,他可能会在两者之间进行推动
  • front() 实现了泄漏抽象。它返回对私有容器的引用,但不保护元素。如果线程 A 调用 front() 并且线程 B 调用 pop_front() 事情就不再好看了。参考是一种责任。代码忽略了这一点。
  • 您也不能将pop 和pop_front 分开。线程 A 和 B 可以先调用front,然后再调用pop_front。然后你处理了一个元素两次,一个根本不处理。
猜你喜欢
  • 1970-01-01
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2012-11-20
  • 2013-10-11
  • 2020-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多