最近学习Qt网络编程时需要用到单生产者多消费者模型,由于socket传输的数据通常被包装成各种不同的结构体(以下都称为消息),在不同的线程中生产和消费,因此考虑使用Qt的信号量编写一个消息队列用于不同线程之间的数据传递,实现单生产者——多消费者模型。
可参考:
- https://blog.csdn.net/jin396932711/article/details/74941500
- https://blog.csdn.net/fanyun_01/article/details/79356833
目录
消息结构
通常生产者生产出的数据类型都为char*(如读取文件线程使用QFile读出的数据),即使用一个指针buf指向该数据的内存。然而在数据传输之前还需要将数据的信息和数据一起打包发送(如数据长度),因此,发送的数据通常被包装成各种形式的结构体。如下为一种包装形式最简单的结构体MSG_PACK,该消息仅包含数据的指针和长度。
//消息MSG结构体
typedef struct
{
char* buf; //数据指针
int bufLen; //数据长度
}MSG_PACK;
消息队列
本文使用QQueue类作为消息队列的容器,使用char型指针指向传入的结构体(也可使用模板),这里需注意的是结构体的字节对齐。若想修改结构体的字节对齐方式(如将结构体字节对齐修改成1),可在第一个结构体前添加:
#pragma pack(1)
并在最后一个结构体后添加:
#pragma pack()
恢复默认字节对齐方式。
blockmsgqueue.h
#ifndef BLOCKMSGQUEUE_H
#define BLOCKMSGQUEUE_H
#include <QSemaphore>
#include <QMutex>
#include <QQueue>
class blockMsgQueue
{
public:
blockMsgQueue(const int& maxMsgsNum, const unsigned int& msgSize);
~blockMsgQueue();
void addMsg(const char* msgPack); //往消息队列添加消息msgPack
void getMsg(char* msgPack); //从消息队列取消息到msgPack
private:
int m_maxMsgsNum; //消息队列容量(最大消息数)
unsigned int m_msgSize; //消息大小
QSemaphore *m_pFreeMsgs; //信号量,队列剩余空间
QSemaphore *m_pUsedMsgs; //信号量,队列已使用空间
QQueue<char*> m_queue; //队列
QMutex m_mutex; //互斥锁
};
#endif // BLOCKMSGQUEUE_H
blockmsgqueue.cpp
#include "blockmsgqueue.h"
blockMsgQueue::blockMsgQueue(const int& maxMsgsNum, const unsigned int& msgSize)
: m_maxMsgsNum(maxMsgsNum),
m_msgSize(msgSize)
{
m_pFreeMsgs = new QSemaphore(maxMsgsNum);
m_pUsedMsgs = new QSemaphore(0);
}
blockMsgQueue::~blockMsgQueue()
{
}
void blockMsgQueue::addMsg(const char* msgPack)
{
m_pFreeMsgs->acquire(1); //队列满时阻塞
m_mutex.lock(); //使用互斥锁,生产者加入数据时消费者不能取出
char* msg = new char[m_msgSize];
memcpy(msg, msgPack, m_msgSize);
m_queue.enqueue(msg);
m_mutex.unlock();
m_pUsedMsgs->release(1);
}
void blockMsgQueue::getMsg(char* msgPack)
{
m_pUsedMsgs->acquire(1); //队列空时阻塞
m_mutex.lock(); //使用互斥锁,多个消费者不能同时取出
char *pack = m_queue.dequeue();
memcpy(msgPack, pack, m_msgSize);
m_mutex.unlock();
m_pFreeMsgs->release(1);
}
main.cpp
#include <QCoreApplication>
#include <blockmsgqueue.h>
#include <QThread>
#include <QDebug>
#include <QtCore>
const int msgsNum = 170; //消息数量
const int maxQueueSize = 10; //消息队列容量
int comsumer_i = 0; //消费者线程
static int takei = 0; //消费者线程计数
//消息MSG结构体
typedef struct
{
char* buf; //数据
unsigned int bufLen; //数据长度
}MSG_PACK;
//消息队列
blockMsgQueue queue(maxQueueSize, sizeof(MSG_PACK));
[生产者]
class Producer : public QThread
{
public:
void run() override
{
//生产者生产170个消息
for(int i=0; i<msgsNum; i++)
{
//模拟生产消息MSGMSG_PACK
MSG_PACK pack;
pack.bufLen = 8;
pack.buf = new char[pack.bufLen];
char str[8] = {0};
for (int j = 0; j < pack.bufLen - 1; j++)
{
str[j] = "ABCDE12345"[QRandomGenerator::global()->bounded(10)];
}
memcpy(pack.buf, str, pack.bufLen);
//加入消息队列
queue.addMsg((char*)&pack);
qDebug() << QString("produce pack %1: %2")
.arg(i+1).arg(pack.buf);
msleep(30);
}
}
};
[消费者]
class Consumer : public QThread
{
public:
void run() override
{
comsumer_i++;
int c_i = comsumer_i;
for(int i=0; i<msgsNum; i++)
{
MSG_PACK pack;
queue.getMsg((char*)&pack);
takei++;
qDebug() << QString("comsumer %1 take pack %2: %3")
.arg(c_i).arg(takei).arg(pack.buf);
delete[] pack.buf;
pack.buf = nullptr;
msleep(100);
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Producer producer; //生产者
Consumer consumer1; //消费者
Consumer consumer2;
Consumer consumer3;
producer.start();
consumer1.start();
consumer2.start();
consumer3.start();
producer.wait();
consumer1.wait();
consumer2.wait();
consumer3.wait();
return a.exec();
}
本程序模拟生产170个消息存入消息队列,在消息传入队列时进行了类型转换,将消息的指针转换为char型指针,并在消息队列内部进行内存拷贝,这样便可适应于不同的结构体(这里笔者试过不进行拷贝而是直接将指针添加进队列,程序崩掉了...)。使用完消息后记得将消息里buf指向的内存delete掉。
运行结果如图:
改进消息队列
上一节所实现的消息队列每次在有新的消息进入队列时都会new一块内存进行拷贝,而从消息队列中取消息时通常是在其它线程(可能不止一个),在使用完消息后若忘记释放则最终会导致内存溢出(这是比较容易出现的问题),这里比较好的一个解决办法就是循环使用同一块内存,即在消息队列(类)构造时就根据其容量和大小分配好内存空间,每次从该队列中取出一个消息后又重新将其加入队列尾,达到一个循环使用内存的状态。
要实现这种机制需要增加一个空队列,在类构造时初始化空队列的内存空间,每次向消息队列中添加消息时都是对空队列中的内存空间进行拷贝,使用完后又重新将指向该内存空间的指针加入空队列的队尾,具体代码如下。
blockmsgqueue.h
#ifndef BLOCKMSGQUEUE_H
#define BLOCKMSGQUEUE_H
#include <QSemaphore>
#include <QMutex>
#include <QQueue>
class blockMsgQueue
{
public:
blockMsgQueue(const int& maxMsgsNum, const unsigned int& msgSize);
~blockMsgQueue();
void addMsg(const char* msgPack); //往消息队列添加消息msgPack
void getMsg(char* msgPack); //从消息队列取消息到msgPack
private:
int m_maxMsgsNum; //消息队列容量(最大消息数)
unsigned int m_msgSize; //消息大小
QSemaphore *m_pFreeMsgs; //信号量,队列剩余空间
QSemaphore *m_pUsedMsgs; //信号量,队列已使用空间
QQueue<char*> m_freeQueue; //空队列
QQueue<char*> m_queue; //消息队列
QMutex m_mutex; //互斥锁
};
#endif // BLOCKMSGQUEUE_H
blockmsgqueue.cpp
#include "blockmsgqueue.h"
#include <QDebug>
blockMsgQueue::blockMsgQueue(const int& maxMsgsNum, const unsigned int& msgSize)
: m_maxMsgsNum(maxMsgsNum),
m_msgSize(msgSize)
{
m_pFreeMsgs = new QSemaphore(maxMsgsNum);
m_pUsedMsgs = new QSemaphore(0);
//初始化空队列
for(int i=0; i<m_maxMsgsNum; i++)
{
char* freeMsg = new char[m_msgSize];
m_freeQueue.enqueue(freeMsg);
}
}
blockMsgQueue::~blockMsgQueue()
{
//释放信号量
if(m_pFreeMsgs)
{
m_pFreeMsgs = nullptr;
}
if(m_pUsedMsgs)
{
m_pUsedMsgs = nullptr;
}
//释放队列的内存
QQueue<char*>::iterator ite1;
QQueue<char*>::iterator ite2;
for(ite1 = m_freeQueue.begin(); ite1 != m_freeQueue.end(); ++ite1)
{
delete[] (*ite1); //delete[] freeMsg
(*ite1) = nullptr;
}
for(ite2 = m_queue.begin(); ite2 != m_queue.end(); ++ite2)
{
delete[] (*ite2); //delete[] freeMsg
(*ite2) = nullptr;
}
}
void blockMsgQueue::addMsg(const char* msgPack)
{
m_pFreeMsgs->acquire(1); //队列满时阻塞
m_mutex.lock(); //使用互斥锁,生产者加入数据时消费者不能取出
char* msgIn = m_freeQueue.dequeue(); //从空队列取出一段指向消息内存的指针
memcpy(msgIn, msgPack, m_msgSize);
m_queue.enqueue(msgIn);
m_mutex.unlock();
m_pUsedMsgs->release(1);
}
void blockMsgQueue::getMsg(char* msgPack)
{
m_pUsedMsgs->acquire(1); //队列空时阻塞
m_mutex.lock(); //使用互斥锁,多个消费者不能同时取出
char *msgOut = m_queue.dequeue();
memcpy(msgPack, msgOut, m_msgSize);
m_freeQueue.enqueue(msgOut); //将消息重新加入空队列,消息指向的内存重复使用
m_mutex.unlock();
m_pFreeMsgs->release(1);
}
消息队列析构时再统一释放内存空间,不再由外部进行操作。
本文所实现的消息队列的具体应用可参考我的另一篇博客:https://blog.csdn.net/qq_38318941/article/details/102651562
有问题和建议欢迎一起学习讨论!