在上一篇文章中基于Qt实现了生产者——消费者模型中使用的消息队列,这篇文章主要以文件数据拷贝为例,模拟数据传输过程中的生产者和消费者,看其是如何工作的。
由于生产者和消费者通常属于不同的线程,因此本文将文件的读和写也分为两个线程,分别为readFileThread和writeFileThread,其中readFileThread充当生产者生产数据,writeFileThread充当消费者从队列中取数据。消息的结构体和上一篇文章所使用的相同,仅包含数据和数据长度。
下图所示为本例的数据传输模式,生产者线程从文件中按固定长度读取数据,将数据和数据长度等信息打包成消息结构体MSG_PACK,并加入消息队列,消费者线程则从队列中取出消息并解析,并将数据写入临时文件File.ing,文件拷贝完成后修改文件后缀为初始类型.ext。
要想保证消费者线程中的数据解析正确,就要使用和生产者线程相同的消息结构体,因此本文将结构体定义在一个头文件中供两个线程调用,具体如下,其中还写入了文件读取和写入的路径,以及队列的容量。
HeadDefine.h
#ifndef HEADDEFINE_H
#define HEADDEFINE_H
#include <QString>
//消息结构体
typedef struct
{
char* buf; //数据
unsigned int bufLen; //数据长度
}MSG_PACK;
const int MaxMsgsNum = 200; //消息队列容量
const QString inFilePath = "D:/Program/Qt Project/fileRead_AND_Write/movie.mkv";
const QString outFilePath = "D:/Program/Qt Project/fileRead_AND_Write/movie_copy.ing";
#endif // HEADDEFINE_H
下面来看生产者线程:
readFileThread.h
#ifndef READFILETHREAD_H
#define READFILETHREAD_H
#include <QThread>
class blockMsgQueue;
class QFile;
class readFileThread : public QThread
{
Q_OBJECT
public:
readFileThread();
~readFileThread() override;
void setFilePath(const QString filePath); //设置文件路径
qint64 getFileSize() const; //获取文件大小
void stop(); //停止线程
bool setMsgQueue(blockMsgQueue *pMsg); //设置消息队列
protected:
void run() override;
private:
qint64 m_fileSize; //文件大小
QString m_filePath; //文件路径
QFile *m_pFile; //文件指针
blockMsgQueue *m_pReadFileMsgQueue; //消息队列
static const qint64 OnePackSize = 1024 * 1024 * 2; //每个消息的数据大小 2M
};
#endif // READFILETHREAD_H
readFileThread.cpp
#include "readfilethread.h"
#include <QFile>
#include <QDebug>
#include "HeadDefine.h"
#include "blockmsgqueue.h"
#include "windows.h"
const qint64 readFileThread::OnePackSize; //类的静态常量定义式
//根据编译器情况决定是否省去这一条
readFileThread::readFileThread()
: QThread(),
m_fileSize(0)
{
m_pReadFileMsgQueue = nullptr;
m_filePath = "";
m_pFile = nullptr;
}
readFileThread::~readFileThread()
{
if(m_pFile)
delete m_pFile;
qDebug() << "Read file thread has been deleted!";
}
void readFileThread::setFilePath(const QString filePath)
{
m_filePath = filePath;
m_pFile = new QFile(filePath);
if(!m_pFile->exists())
{
qDebug() << "Read file thread"
<< " File: " << filePath << "is not exists!";
return;
}
m_fileSize = m_pFile->size(); //读文件时能够知道文件的大小
}
qint64 readFileThread::getFileSize() const
{
return m_fileSize;
}
void readFileThread::stop()
{
requestInterruption();
}
bool readFileThread::setMsgQueue(blockMsgQueue *pMsg)
{
m_pReadFileMsgQueue = pMsg;
if(m_pReadFileMsgQueue == nullptr)
{
qDebug() << "Read file thread: Message queue initialize failed!";
return false;
}
else
return true;
}
void readFileThread::run()
{
bool bRet = m_pFile->open(QFile::ReadOnly);
if(!bRet)
{
qDebug() << "Read file thread(" << GetCurrentThreadId() << "): "
<< "Open File failed!";
return;
}
qint64 readBytes = 0;
while(!isInterruptionRequested())
{
msleep(10);
if(m_pReadFileMsgQueue == nullptr)
{
qDebug() << "Read file thread(" << GetCurrentThreadId() << "): "
<< "Message queue is not initialized!";
break;
}
char *tempBuf = new char[OnePackSize]; //临时缓存
qint64 bytesReadOne = m_pFile->read(tempBuf, OnePackSize); //读文件数据
MSG_PACK msgPack;
msgPack.bufLen = qMin(OnePackSize, bytesReadOne);
msgPack.buf = new char[msgPack.bufLen];
memcpy(msgPack.buf, tempBuf, msgPack.bufLen); //从临时缓存区拷贝数据到消息
readBytes += msgPack.bufLen; //读取的数据总量
m_pReadFileMsgQueue->addMsg((char*)&msgPack); //加入队列
qDebug() << QString("File reading progress: %1 %, readBytes %2")
.arg(int(((double)readBytes) / ((double)m_fileSize) * 100))
.arg(msgPack.bufLen); //打印进度
delete [] tempBuf;
tempBuf = nullptr;
//读取结束
if(readBytes >= m_fileSize)
{
m_pFile->close();
qDebug() << "Reading file completes!";
break;
}
}
//线程结束时关闭文件
if(m_pFile->isOpen())
{
m_pFile->close();
}
}
该线程构造时初始化相应参数,文件路径通过在外部调用实例化后的方法获取,通过该路径实例化一个QFile对象后便可知道该文件的大小。这一步很重要,因为在拷贝之前必须要告诉消费者线程该文件的大小,否者消费者不知道何时该停止拷贝。
下面看消费者线程:
writeFileThread.h
#ifndef WRITEFILETHREAD_H
#define WRITEFILETHREAD_H
#include <QThread>
class blockMsgQueue;
class QFile;
class writeFileThread : public QThread
{
Q_OBJECT
public:
writeFileThread();
~writeFileThread() override;
void setFilePath(const QString filePath); //设置文件路径
qint64 getFileSize() const; //获取文件大小
void stop(); //停止线程
bool setMsgQueue(blockMsgQueue *pMsg); //设置消息队列
void acquireFileSize(qint64 fileSize); //获取文件大小
protected:
void run() override;
private:
qint64 m_fileSize; //文件大小
QString m_filePath; //文件路径
QFile *m_pFile; //文件指针
blockMsgQueue *m_pWriteFileMsgQueue; //消息队列
static const qint64 OnePackSize = 1024 * 1024 * 2; //每个消息读取的数据大小 2M
};
#endif // WRITEFILETHREAD_H
writeFileThread.cpp
#include "writefilethread.h"
#include <QFile>
#include <QFileInfo>
#include <QDebug>
#include "HeadDefine.h"
#include "blockmsgqueue.h"
#include "windows.h"
const qint64 writeFileThread::OnePackSize; //类的静态常量定义式
//根据编译器情况决定是否省去这一条
writeFileThread::writeFileThread()
: QThread(),
m_fileSize(0)
{
m_filePath = "";
m_pFile = nullptr;
m_pWriteFileMsgQueue = nullptr;
}
writeFileThread::~writeFileThread()
{
if(m_pFile)
delete m_pFile;
qDebug() << "Write file thread has been deleted!";
}
void writeFileThread::setFilePath(const QString filePath)
{
m_filePath = filePath;
m_pFile = new QFile(filePath);
if(!m_pFile->exists())
{
qDebug() << "Read file thread"
<< " File: " << filePath << "is not exists!";
}
}
qint64 writeFileThread::getFileSize() const
{
return m_fileSize;
}
void writeFileThread::stop()
{
requestInterruption();
}
bool writeFileThread::setMsgQueue(blockMsgQueue *pMsg)
{
m_pWriteFileMsgQueue = pMsg;
if(m_pWriteFileMsgQueue == nullptr)
{
qDebug() << "Read file thread: "
<< "Message queue initialize failed!";
return false;
}
else
return true;
}
void writeFileThread::acquireFileSize(qint64 fileSize)
{
m_fileSize = fileSize; //通过读入的文件获取文件大小
if(m_fileSize <= 0)
{
qDebug() << "Write file thread: File is empty!";
}
}
void writeFileThread::run()
{
bool bRet = m_pFile->open(QFile::WriteOnly);
if(!bRet)
{
qDebug() << "Write file thread(" << GetCurrentThreadId() << "): "
<< "Open File failed!";
return;
}
qint64 writtenBytes = 0;
while(!isInterruptionRequested())
{
msleep(20);
if(m_pWriteFileMsgQueue == nullptr)
{
qDebug() << "Write file thread(" << GetCurrentThreadId() << "): "
<< "Message queue is not initialized!";
break;
}
MSG_PACK msgPack;
m_pWriteFileMsgQueue->getMsg((char*)&msgPack); //获取消息队列中的数据
qint64 bytesWrittenOne = m_pFile->write(msgPack.buf, msgPack.bufLen); //写入文件
writtenBytes += bytesWrittenOne;
qDebug() << QString("File Writing progress: %1 %, writtenBytes %2")
.arg(int(((double)writtenBytes) / ((double)m_fileSize) * 100))
.arg(msgPack.bufLen); //打印进度
delete [] msgPack.buf;
msgPack.buf = nullptr;
//拷贝结束
if(writtenBytes >= m_fileSize)
{
QFileInfo fi(m_filePath);
QStringList list = m_filePath.split("/");
QString newName = "";
for(int i=0; i<list.size()-1; i++)
{
newName += list.at(i);
newName += QString("/"); //重新组装
}
newName += fi.baseName();
newName += QString(".mkv");
if(QFile::exists(newName))
{
QFile::remove(newName); //删除同名文件
}
m_pFile->rename(newName); //修改文件名
m_pFile->close();
qDebug() << "Writing file completes!";
break;
}
}
//线程结束时关闭文件
if(m_pFile->isOpen())
{
m_pFile->close();
}
}
与生产者线程不同的是,消费者线程需要从生产者线程那知道拷贝文件的大小,因此该线程新加了一个方法acquireFileSize用于获取文件大小。在文件拷贝的过程中数据一致写入在临时文件.ing,因此在完成拷贝后需要将文件后缀名改成与原始文件相同的后缀,这里用到了QFileInfo类和QStringList类,QFileInfo类可获取不包含后缀的文件名,将文件路径根据‘ / ’拆分后的结果可保存在QStringList中,并进行重组,以完成文件后缀的修改,其中QStringList的最后一个元素正好是文件名+后缀。
本文对一个视频文件(.mkv)进行拷贝,执行的操作如下:
main.cpp
#include <QCoreApplication>
#include "readfilethread.h"
#include "writefilethread.h"
#include "HeadDefine.h"
#include "blockmsgqueue.h"
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
//初始化队列
blockMsgQueue *msgQueue = new blockMsgQueue(MaxMsgsNum, sizeof (MSG_PACK));
readFileThread readThread; //读线程
writeFileThread writeThread; //写线程
readThread.setFilePath(inFilePath);
readThread.setMsgQueue(msgQueue);
writeThread.setFilePath(outFilePath);
writeThread.acquireFileSize(readThread.getFileSize());
writeThread.setMsgQueue(msgQueue);
QObject::connect(&readThread, &QThread::finished, &readThread, &QObject::deleteLater);
QObject::connect(&writeThread, &QThread::finished, &writeThread, &QObject::deleteLater);
readThread.start();
writeThread.start();
//手动结束线程
// readThread.stop();
// writeThread.stop();
return a.exec();
}
在文件读写结束后线程对象会自动销毁,执行的结果见下图:
在设置参数时注意队列的大小和每包读取的数据长度,虽然数据保存在堆上但依然要注意不要超过操作系统允许的上限值,这里的视频文件较大,为了拷贝速度所以每包读取的数据较多,队列容量设小一点其实没什么影响。