我创建了一个通用消息队列,用于多线程应用程序。具体而言,单一生产者,多消费者。主要代码如下。
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作为参考来实现我想要的吗?
答案 0 :(得分:1)
编辑(我添加了第1,2和4部分的答案)。
1)您应该有一个工厂方法来创建新的genericMsgs并返回std::unique_ptr
。绝对没有理由通过const引用传递genericMsg然后让队列将它包装在一个智能指针中:一旦你通过引用传递你已经失去了所有权的跟踪,所以如果你这样做,队列将会有构造(通过复制)整个genericMsg来包装。
2)我无法想到任何可以安全地引用shared_ptr
或unique_ptr
或auto_ptr
的情况。 shared_ptrs和unique_ptrs用于跟踪所有权,一旦你引用它们(或它们的地址),你就不知道有多少引用或指针仍在那里,期望shared_ptr / unique_ptr对象包含有效的裸指针。
unique_ptr总是优先于裸指针,并且在一次只有一段代码(有效地)指向一个对象的情况下,它优先于shared_ptr。
3)是的,您需要在std::condition_variable
功能中使用dequeue
。在调用qData
或qData.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
需要锁定。