如何从Qt中的大文件异步加载数据?

时间:2016-01-03 00:07:43

标签: c++ multithreading qt io

我使用Qt 5.2.1来实现从文件中读取数据的程序(可能是几个字节到几GB),并以一种依赖于每个数据的方式显示数据字节。我的例子是一个十六进制查看器。

一个对象执行读取操作,并在读取新数据块时发出信号dataRead()。信号带有指向QByteArray的指针,如下所示:

filereader.cpp

void FileReader::startReading()
{

    /* Object state code here... */

        {
            QFile inFile(fileName);

            if (!inFile.open(QIODevice::ReadOnly))
            {
                changeState(STARTED, State(ERROR, QString()));
                return;
            }

            while(!inFile.atEnd())
            {
                QByteArray *qa = new QByteArray(inFile.read(DATA_SIZE));
                qDebug() << "emitting dataRead()";
                emit dataRead(qa);
            }
        }

    /* Emit EOF signal */

}

查看器的loadData插槽连接到此信号,这是显示数据的功能:

hexviewer.cpp

void HexViewer::loadData(QByteArray *data)
{
    QString hexString = data->toHex();

    for (int i = 0; i < hexString.length(); i+=2)
    {
        _ui->hexTextView->insertPlainText(hexString.at(i));
        _ui->hexTextView->insertPlainText(hexString.at(i+1));
        _ui->hexTextView->insertPlainText(" ");
    }

    delete data;
}

第一个问题是,如果只是按原样运行,GUI线程将完全没有响应。在重新绘制GUI之前,将发出所有dataRead()个信号。

full code可以运行,当你使用大于1kB的文件时,你会看到这种行为。)

通过对我的论坛帖子Non-blocking local file IO in Qt5的回复和另一个Stack Overflow问题How to do async file io in qt?的答案,答案是:使用线程。但这些答案都没有详细说明如何改变数据本身,也不知道如何避免常见的错误和陷阱。

如果数据很小(大约一百字节),我只是用信号发出它。但是,如果文件大小为GB(编辑,或者文件位于基于网络的文件系统,例如。 NFS,Samba共享,我不希望UI因为读取文件块而锁定。

第二个问题是在发射器中使用new和在接收器中使用delete的机制看起来有点天真:我有效地将整个堆用作跨线程队列

问题1: Qt是否有更好的/惯用的方式在线程间移动数据,同时限制内存消耗?它是否有一个线程安全队列或其他可以简化整个过程的结构?

问题2:是否自己实施线程等?我不是重新发明轮子的忠实粉丝,尤其关于内存管理和线程。是否有更高级别的构造可以做到这一点,就像网络传输一样?

4 个答案:

答案 0 :(得分:7)

首先,您的应用中根本没有任何多线程。您的FileReader类是QThread的子类,但并不意味着所有FileReader方法都将在另一个线程中执行。实际上,所有操作都在主(GUI)线程中执行。

FileReader应该是QObject而不是QThread子类。然后,您创建一个基本的QThread对象,并使用QObject::moveToThread将您的工作者(读者)移动到该对象。您可以阅读此技术here

确保您使用FileReader::State注册了qRegisterMetaType类型。这对于Qt信号槽连接在不同线程上工作是必要的。

一个例子:

HexViewer::HexViewer(QWidget *parent) :
    QMainWindow(parent),
    _ui(new Ui::HexViewer),
    _fileReader(new FileReader())
{
    qRegisterMetaType<FileReader::State>("FileReader::State");

    QThread *readerThread = new QThread(this);
    readerThread->setObjectName("ReaderThread");
    connect(readerThread, SIGNAL(finished()),
            _fileReader, SLOT(deleteLater()));
    _fileReader->moveToThread(readerThread);
    readerThread->start();

    _ui->setupUi(this);

    ...
}

void HexViewer::on_quitButton_clicked()
{
    _fileReader->thread()->quit();
    _fileReader->thread()->wait();

    qApp->quit();
}

此处没有必要在堆上分配数据:

while(!inFile.atEnd())
{
    QByteArray *qa = new QByteArray(inFile.read(DATA_SIZE));
    qDebug() << "emitting dataRead()";
    emit dataRead(qa);
}

QByteArray使用implicit sharing。这意味着当您以只读模式跨函数传递QByteArray对象时,不会一次又一次地复制其内容。

将上面的代码更改为此并忘记手动内存管理:

while(!inFile.atEnd())
{
    QByteArray qa = inFile.read(DATA_SIZE);
    qDebug() << "emitting dataRead()";
    emit dataRead(qa);
}

但无论如何,主要问题不在于多线程。问题是QTextEdit::insertPlainText操作并不便宜,尤其是当您拥有大量数据时。 FileReader可以非常快速地读取文件数据,然后使用要显示的新数据部分填充您的窗口小部件。

必须注意的是,HexViewer::loadData的实施效果非常差。您可以通过char插入文本数据char,这会使QTextEdit不断重绘其内容并冻结GUI。

您应首先准备生成的十六进制字符串(请注意,数据参数不再是指针):

void HexViewer::loadData(QByteArray data)
{
    QString tmp = data.toHex();

    QString hexString;
    hexString.reserve(tmp.size() * 1.5);

    const int hexLen = 2;

    for (int i = 0; i < tmp.size(); i += hexLen)
    {
        hexString.append(tmp.mid(i, hexLen) + " ");
    }

    _ui->hexTextView->insertPlainText(hexString);
}

无论如何,您的应用程序的瓶颈不是文件读取,而是QTextEdit更新。按块加载数据,然后使用QTextEdit::insertPlainText将其附加到窗口小部件将不会加快任何速度。对于小于1Mb的文件,一次读取整个文件然后在一步中将结果文本设置为小部件会更快。

我认为使用默认的Qt小部件,您无法轻松显示大于几兆字节的大文本。此任务需要一些非平凡的approch,通常与多线程或异步数据加载无关。所有这些都是关于创建一些棘手的小部件,它不会立刻尝试显示其庞大的内容。

答案 1 :(得分:2)

这似乎是您希望拥有一个具有信号量的消费者生产者的情况。有一个非常具体的example可以指导您正确实施它。你还需要一个线程来使这个工作与你的主线程分开。

设置应该是:

  • 线程A将文件读取器作为生产者运行
  • 您的GUI线程运行您的Hexviewer小部件,该小部件使用特定事件的数据。 在发出QSemaphore::acquire()之前,我需要检查QSemaphore :: available()`以确保,以避免阻止GUI。
  • Filereader和Hexviewer可以访问第三个类,例如DataClass,数据在读取和从消费者检索时放置。这也应该定义信号量。
  • 无需使用数据发出信号或通知。

这几乎涵盖了将数据读取从文件读取器移动到窗口小部件,但它没有涵盖如何实际绘制此数据。为了实现这一目标,您可以通过覆盖Hexviewer的绘制事件并读取已放入队列中的内容来消耗痛苦的数据。更精细的方法是编写an event filter

除此之外,您可能希望读取最大字节数,然后显式信号通知Hexviewer使用数据。

请注意,此解决方案完全异步,线程安全且有序,因为您的数据都没有发送到Hexviewer,但Hexviewer只在需要在屏幕上显示时消耗它。

答案 2 :(得分:0)

  1. 如果您计划编辑忘记QTextEdit的10GB文件。这个ui->hexTextView->insertPlainText只会占用整个内存,然后才能读取文件的1/10。 IMO您应该使用QTableView来呈现和编辑数据。为此,您应该继承QAbstractTableModel。在一行中,您应该呈现16个字节。在十六进制形式的前16列和ASCII形式的下一列中。这不应该是复杂的。只是阅读QAbstractTableModel的可怕文档。缓存数据在这里最重要。如果我有时间,我会给出代码示例。

  2. 忘了使用多个线程。这是使用这种事情的坏情况,并且很可能会产生许多与同步相关的问题。

  3. 好的,我在这里有一段时间是有效的代码(我测试它运作顺利):

    #include <QObject>
    #include <QFile>
    #include <QQueue>
    
    class LargeFileCache : public QObject
    {
        Q_OBJECT
    public:
        explicit LargeFileCache(QObject *parent = 0);
    
        char geByte(qint64 pos);
        qint64 FileSize() const;
    
    signals:
    
    public slots:
        void SetFileName(const QString& filename);
    
    private:
        static const int kPageSize;
    
        struct Page {
            qint64 offset;
            QByteArray data;
        };
    
    private:
        int maxPageCount;
        qint64 fileSize;
    
        QFile file;
        QQueue<Page> pages;
    };
    
    #include <QAbstractTableModel>
    
    class LargeFileCache;
    
    class LageFileDataModel : public QAbstractTableModel
    {
        Q_OBJECT
    public:
        explicit LageFileDataModel(QObject *parent);
    
        // QAbstractTableModel
        int rowCount(const QModelIndex &parent) const;
        int columnCount(const QModelIndex &parent) const;
        QVariant data(const QModelIndex &index, int role) const;
    
    signals:
    
    public slots:
        void setFileName(const QString &fileName);
    
    private:
        LargeFileCache *cachedData;
    };
    
    #include "lagefiledatamodel.h"
    #include "largefilecache.h"
    
    static const int kBytesPerRow = 16;
    
    LageFileDataModel::LageFileDataModel(QObject *parent)
        : QAbstractTableModel(parent)
    {
        cachedData = new LargeFileCache(this);
    }
    
    int LageFileDataModel::rowCount(const QModelIndex &parent) const
    {
        if (parent.isValid())
            return 0;
        return (cachedData->FileSize() + kBytesPerRow - 1)/kBytesPerRow;
    }
    
    int LageFileDataModel::columnCount(const QModelIndex &parent) const
    {
        if (parent.isValid())
            return 0;
        return kBytesPerRow;
    }
    
    QVariant LageFileDataModel::data(const QModelIndex &index, int role) const
    {
        if (index.parent().isValid())
            return QVariant();
        if (index.isValid()) {
            if (role == Qt::DisplayRole) {
                qint64 pos = index.row()*kBytesPerRow + index.column();
                if (pos>=cachedData->FileSize())
                    return QString();
                return QString::number((unsigned char)cachedData->geByte(pos), 0x10);
            }
        }
    
        return QVariant();
    }
    
    void LageFileDataModel::setFileName(const QString &fileName)
    {
        beginResetModel();
        cachedData->SetFileName(fileName);
        endResetModel();
    }
    
    #include "largefilecache.h"
    
    const int LargeFileCache::kPageSize = 1024*4;
    
    LargeFileCache::LargeFileCache(QObject *parent)
        : QObject(parent)
        , maxPageCount(1024)
    {
    
    }
    
    char LargeFileCache::geByte(qint64 pos)
    {
        // largefilecache
        if (pos>=fileSize)
            return 0;
    
        for (int i=0, n=pages.size(); i<n; ++i) {
            int k = pos - pages.at(i).offset;
            if (k>=0 && k< pages.at(i).data.size()) {
                pages.enqueue(pages.takeAt(i));
                return pages.back().data.at(k);
            }
        }
    
        Page newPage;
        newPage.offset = (pos/kPageSize)*kPageSize;
        file.seek(newPage.offset);
        newPage.data = file.read(kPageSize);
        pages.push_front(newPage);
    
        while (pages.count()>maxPageCount)
            pages.dequeue();
    
        return newPage.data.at(pos - newPage.offset);
    }
    
    qint64 LargeFileCache::FileSize() const
    {
        return fileSize;
    }
    
    void LargeFileCache::SetFileName(const QString &filename)
    {
        file.close();
        file.setFileName(filename);
        file.open(QFile::ReadOnly);
        fileSize = file.size();
    }
    

    它比我预期的要短,需要一些改进,但它应该是一个很好的基础。

答案 3 :(得分:0)

对于十六进制查看器,我认为您根本走错了轨道-除非您认为它最有可能在具有SCSI或RAID阵列的系统上使用以提高速度。为什么一次要加载千兆字节的数据?这些天来,文件填充文本框的访问非常快。当然,例如Notepad ++具有出色的十六进制查看器插件,您必须首先加载文件;但这是因为可以编辑文件,这就是NPP的工作方式。

我认为您可能会结束对文本框的子类化,获取足够的数据以加载文本框,甚至挥霍,并加载当前位置前后的500k数据。然后,假设您从字节零开始。为您的显示加载足够的数据,也许还有一些额外的数据;但请将滚动条类型设置为始终可见。然后,我认为您可能会通过对QTextBox进行子类化来拦截滚动事件。并编写自己的scrollContentsBy()和changeEvent()和/或paint()事件。

更简单地说,您可以创建一个没有滚动条的QTextBox;以及旁边的QVerticalScrollbar。设置范围和起始值。然后,响应valueChanged()事件;并更改QTextBox的内容。这样,用户不必等待磁盘读取就可以开始编辑,而且资源(即内存)要容易得多,因此,如果打开了许多应用程序,则不会换出到磁盘)。听起来很难归类于这些东西,但是很多时候,似乎比实际要难。通常已经有很好的例子说明有人在做这样的事情。

相比之下,如果有多个线程在读取文件,则可能从头开始读取一个,从中间读取另一个,而从结尾读取另一个。单个读取头会跳来跳去,试图满足所有请求,因此操作效率较低。如果它是SDD驱动器,则非线性读取不会对您造成伤害,但它们也不会对您有帮助。如果您想权衡一下可能需要明显的加载时间,以便用户可以随意滚动很多,这会更快一些(文本框中充满了数据,可以真正读取 加载不需要很长时间),那么您可能有一个单个线程在后台读取它,然后您可以让主要线程继续处理事件循环。更简单的是,一次读取整个整个文件时,一次读入 n 兆字节的块,然后执行qApp->processEvents();来让GUI响应可能发生的任何GUI事件每次读取块后都发生了错误。

如果您愿意相信它很可能会在SCSI或RAID阵列上使用,那么进行多次读取以进行读取可能很有意义。一个SCSI驱动器可以具有多个读取头。某些RAID阵列已设置为将数据分散到多个磁盘中,以提高速度。请注意,如果将RAID阵列设置为保留数据的多个相同副本,则最好使用单个线程进行读取。当我实现多线程时,发现这里提出的轻量级模型最有用:QThread: You were not doing so wrong。我确实必须在结果结构上执行Q_DECLARE_METATYPE,为其定义了构造函数,析构函数和移动运算符(我使用了memmove),并且对结构和向量都执行了qRegisterMetaType()来保存结果,以便它可以正确返回结果。您需要支付它阻塞向量的价格才能返回其结果;但实际上,实际开销似乎并不多。在这种情况下,共享内存可能也是值得追求的-但也许每个线程都可以拥有自己的内存,因此您无需锁定从其他线程结果中读取的内容来编写它。