使用shared_ptrs </unsigned>的类型对<unsigned int,=“”boost :: any =“”>的通用容器的线程安全实现

时间:2013-04-29 20:05:30

标签: c++ multithreading boost stl

我创建了一个通用消息队列,用于多线程应用程序。具体而言,单一生产者,多消费者。主要代码如下。

1)我想知道是否应该通过值将分配了new的shared_ptr传递给enqueue方法,还是让队列包装器分配内存本身并通过const引用传入genericMsg对象更好?

2)我的dequeue方法是否应该返回一个shared_ptr,将一个shared_ptr作为参数通过引用(当前策略)传入,或者让它直接返回一个genericMsg对象?

3)我是否需要在enqueue / dequeue中进行信号/等待或者读/写锁定是否足够?

4)我甚至需要使用shared_ptrs吗?或者这仅仅依赖于我使用的实现?我喜欢一旦所有引用不再使用该对象,shared_ptrs将释放内存。如果推荐的话,我可以很容易地将它移植到常规指针。

5)我在这里存储了一对,因为我想区分我正在处理的消息类型,否则不必进行any_cast。每种消息类型都有一个引用特定结构的唯一ID。有没有更好的方法呢?

通用消息类型:

template<typename Message_T>
class genericMsg
{ 
  public:
    genericMsg()
    {
       id = 0;
       size = 0;
    }

    genericMsg (unsigned int &_id, unsigned int &_size, Message_T &_data)
    {
       id = _id;
       size = _size;
       data = _data;
    }

    ~genericMsg()
    {}

    unisgned int   id;
    unsigned int   size;
    Message_T      data; //All structs stored here contain only POD types
};

入队方法:

  // ----------------------------------------------------------------
  // -- Thread safe function that adds a new genericMsg object to the
  // -- back of the Queue.
  // -----------------------------------------------------------------
  template<class Message_T>
  inline void enqueue(boost::shared_ptr< genericMsg<Message_T> > data)
  {
     WriteLock w_lock(myLock);
     this->qData.push_back(std::make_pair(data->id, data));
  }

VS

  // ----------------------------------------------------------------
  // -- Thread safe function that adds a new genericMsg object to the
  // -- back of the Queue.
  // -----------------------------------------------------------------
  template<class Message_T>
  inline void enqueue(const genericMsg<Message_T> &data_in)
  {
     WriteLock w_lock(myLock);
     boost::shared_ptr< genericMsg<Message_T> > data = 
          new genericMsg<Message_T>(data_in.id, data_in.size, data_in.data);
     this->qData.push_back(std::make_pair(data_in.id, data));
  }

出队方法:

  // ----------------------------------------------------------------
  // -- Thread safe function that grabs a genericMsg object from the
  // -- front of the Queue.
  // -----------------------------------------------------------------
  template<class Message_T>
  void dequeue(boost::shared_ptr< genericMsg<Message_T> > &msg)
  {
     ReadLock r_lock(myLock);
     msg = boost::any_cast< boost::shared_ptr< genericMsg<Message_T> > >(qData.front().second);
     qData.pop_front();
  }

获取消息ID:

  inline unsigned int getMessageID()
  {
     ReadLock r_lock(myLock);
     unsigned int tempID = qData.front().first;
     return tempID;
  }

数据类型:

 std::deque < std::pair< unsigned int, boost::any> > qData;

修改

我的设计有所改进。我现在有一个genericMessage基类,我直接从子类中派生,以便派生独特的消息。

通用消息基类:

class genericMessage
{
   public:
      virtual ~genericMessage() {}
      unsigned int getID() {return id;}
      unsigned int getSize() {return size;}

   protected:
      unsigned int id;
      unsigned int size;   
};

Producer Snippet:

 boost::shared_ptr<genericMessage> tmp (new derived_msg1(MSG1_ID));
 theQueue.enqueue(tmp);

消费者代码:

 boost::shared_ptr<genericMessage> tmp = theQueue.dequeue();         
 if(tmp->getID() == MSG1_ID)
 {
    boost::shared_ptr<derived_msg1> tObj = boost::dynamic_pointer_cast<derived_msg1>(tmp);
    tObj->printData();
 }

新队列:

  std::deque< boost::shared_ptr<genericMessage> > qData;

新入队:

void mq_class::enqueue(const boost::shared_ptr<genericMessage> &data_in)
{
   boost::unique_lock<boost::mutex> lock(mut);
   this->qData.push_back(data_in);
   cond.notify_one();
}

New Dequeue:

boost::shared_ptr<genericMessage> mq_class::dequeue()
{
   boost::shared_ptr<genericMessage> ptr;
   {
      boost::unique_lock<boost::mutex> lock(mut);
      while(qData.empty())
      {
         cond.wait(lock);
      }
      ptr = qData.front();
      qData.pop_front();
   }
   return ptr;
}

现在,我的问题是我正确地出队了吗?还有另一种方法吗?在这种情况下,我应该传入shared_ptr作为参考来实现我想要的吗?

3 个答案:

答案 0 :(得分:1)

编辑(我添加了第1,2和4部分的答案)。

1)您应该有一个工厂方法来创建新的genericMsgs并返回std::unique_ptr。绝对没有理由通过const引用传递genericMsg然后让队列将它包装在一个智能指针中:一旦你通过引用传递你已经失去了所有权的跟踪,所以如果你这样做,队列将会有构造(通过复制)整个genericMsg来包装。

2)我无法想到任何可以安全地引用shared_ptrunique_ptrauto_ptr的情况。 shared_ptrs和unique_ptrs用于跟踪所有权,一旦你引用它们(或它们的地址),你就不知道有多少引用或指针仍在那里,期望shared_ptr / unique_ptr对象包含有效的裸指针。

unique_ptr总是优先于裸指针,并且在一次只有一段代码(有效地)指向一个对象的情况下,它优先于shared_ptr。

3)是的,您需要在std::condition_variable功能中使用dequeue。在调用qDataqData.front()之前,您需要测试qData.pop_front()是否为空。如果qData为空,则需要等待条件变量。当enqueue插入一个项目时,它应该通知条件变量以唤醒可能一直在等待的任何人。

您对读取器/写入器锁的使用完全不正确。不要使用读/写锁。使用std::mutex。读卡器锁只能用于完全const的方法。您正在修改qData中的dequeue,因此读者锁定将导致数据竞争。 (读取器编写器锁定仅适用于具有const 长时间保持锁定的愚蠢代码。您只需保持锁定一段时间即可插入或删除锁定队列,所以即使你是const,读者/作者锁定的额外开销将是一个净损失。)

使用互斥锁和condition_variables实现(有界)缓冲区的示例可在以下位置找到:Is this a correct way to implement a bounded buffer in C++

4)unique_ptr总是优先于裸指针,通常优先于shared_ptr。 (shared_ptr可能更好的主要例外是类似于图形的数据结构。)在像你这样的情况下你正在阅读一些东西,用工厂创建一个新对象,将所有权移到队列然后移出所有权对于消费者来说,你应该使用unique_ptr。

5)你正在重塑tagged unions。虚拟函数专门添加到c ++中,因此您不需要这样做。您应该从具有名为do_it()的虚拟函数(或更好,operator()()或类似的东西)的类中继承消息。然后,不是标记每个结构,而是使每个结构成为消息类的子类。当您将每个结构(或ptr结构化)取消时,只需在其上调用do_it()即可。强静态打字,无强制转换。有关示例,请参阅C++ std condition variable covering a lot of share variables

另外:如果您要坚持使用已标记的联合:您无法单独调用以获取ID和数据项。考虑:如果线程A调用获取id,那么线程B调用获取id,然后线程B检索数据项,现在当线程A调用检索数据项时会发生什么?它获取一个数据项,但不是它预期的类型。您需要检索相同关键部分下的ID和数据项。

答案 1 :(得分:1)

首先,最好使用第三方并发容器,而不是自己实现它们,除非是出于教育目的。

您的消息看起来没有昂贵的构造函数/析构函数,因此您可以按值存储它们并忘记所有其他问题。使用移动语义(如果可用)进行优化。

如果你的探查者说“按价值”在你的特定情况下是个坏主意:

我想你的制作人创建了消息,将它们放入你的队列并且对它们失去了兴趣。在这种情况下,您不需要shared_ptr,因为您没有共享所有权。您可以使用unique_ptr甚至是原始指针。它是实现细节,最好将它们隐藏在队列中。

从性能的角度来看,最好实现lock-free queue。 “锁定与信号”完全取决于您的应用程序。例如,如果您使用线程池和调度程序的类型,最好允许您的客户端在队列满/空时执行一些有用的操作。在更简单的情况下,读/写锁就可以了。

答案 2 :(得分:0)

如果我想要线程安全,我通常使用const对象并仅在复制或创建构造函数上进行修改。这样您就不需要使用任何锁定机制。在线程系统中,它通常比在单个实例上使用mutex更有效。

在您的情况下,只有deque需要锁定。