基于Qt模拟生产者——消费者模型:文件数据拷贝

上一篇文章中基于Qt实现了生产者——消费者模型中使用的消息队列,这篇文章主要以文件数据拷贝为例,模拟数据传输过程中的生产者和消费者,看其是如何工作的。

由于生产者和消费者通常属于不同的线程,因此本文将文件的读和写也分为两个线程,分别为readFileThreadwriteFileThread,其中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();
}

在文件读写结束后线程对象会自动销毁,执行的结果见下图:

拷贝过程

 

拷贝结束
控制台

在设置参数时注意队列的大小和每包读取的数据长度,虽然数据保存在堆上但依然要注意不要超过操作系统允许的上限值,这里的视频文件较大,为了拷贝速度所以每包读取的数据较多,队列容量设小一点其实没什么影响。

 

 

 

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值