【问题标题】:How can I asynchronously load data from large files in Qt?如何从 Qt 中的大文件中异步加载数据?
【发布时间】:2016-04-06 22:30:27
【问题描述】:

我正在使用 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 线程将完全没有响应。所有的dataRead() 信号都将在重新绘制 GUI 之前发出。

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

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

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

第二个问题是在发射器中使用new 和在接收器中使用delete 的机制似乎有点幼稚:我有效地将整个堆用作跨线程队列。

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

问题 2:我是否必须自己实现线程等?我不是重新发明轮子的忠实粉丝,尤其是在内存管理和线程方面。是否有更高级别的结构已经可以做到这一点,比如网络传输?

【问题讨论】:

  • 关于问题2:看看QtConcurrent::run。它允许你异步执行一个函数(或成员函数)。
  • 我建议使用工作线程(例如“详细描述”部分中的here)。关于堆分配,我看不到它的需要,只需传递一个 const ref。实现一个控制器(我想是你的 HexViewer)和工作线程,如示例所示

标签: c++ multithreading qt io


【解决方案1】:

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

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

确保您已使用qRegisterMetaType 注册FileReader::State 类型。这是 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 的实现非常无效。您逐个字符地插入文本数据,这使得QTextEdit 不断重绘其内容并冻结 GUI。

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

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 小部件轻松显示大于几兆字节的大文本。此任务需要一些重要的方法,通常与多线程或异步数据加载无关。这一切都是为了创建一些不会尝试立即显示其巨大内容的棘手小部件。

【讨论】:

  • 第一点:很好地抓住了QThread 继承。我知道它不会使我的应用程序成为多线程的,它是我最初尝试使某些东西工作时遗留下来的。我特别避免让演示代码线程化,以避免基于我开始的可能错误方法的答案。
  • 第二(和主要)点:我担心在FileReader 发出多个信号之前会发生什么情况@ 987654347@ 处理它们(例如,由于怪癖线程,也许 UI 线程不会在每次发射时唤醒)。 QByteArray 能解决这个问题吗?通过阅读有关隐式共享的文档,我了解到,是的,它应该在信号发射器或接收器的范围内徘徊,并在 (a) 发射器范围关闭和 (b) 接收器范围关闭时被删除。对吗?
  • 但是中间有一个编组过程(让它跨越线程边界),我对此了解的不够多,无法对此进行推理。
  • 第三点:从我最初的抓挠来看,FileReader::State 似乎可以很好地跨越线程边界。我是在追求未定义的行为,还是 Qt5 自动处理基本枚举类型?
  • 4. FileReader 的当前实现不会等待某人处理其数据。目前FileReader 只是读取文件,然后将其数据块发送到任何地方,仅此而已。如果有侦听器,它最终会捕获该数据。您想要的行为假设应该有某种请求-响应交互。小部件请求一些数据,从阅读器获得响应,显示数据,然后发送一个它想要另一部分的新请求。
【解决方案2】:
  1. 如果您打算编辑 10GB 文件,忘记了 QTextEdit。这个ui-&gt;hexTextView-&gt;insertPlainText 只会在你读取文件的 1/10 之前吃掉整个内存。 IMO 你应该使用QTableView 来呈现和编辑数据。为此,您应该继承QAbstractTableModel。在一行中,您应该呈现 16 个字节。在十六进制形式的前 16 列中,在 ASCII 形式的下一列中。这不应该太复杂。只需仔细阅读QAbstractTableModel 的文档即可。缓存数据在这里将是最重要的。如果有时间我会给出代码示例。

  2. 忘记了使用多线程。使用这样的东西是不好的情况,很可能你会产生很多与同步相关的问题。

好的,我在这里有一些时间是可以正常工作的代码(我已经测试过它可以正常工作):

#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();
}

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

【讨论】:

  • 我宁愿不关注 UI 本身。十六进制查看器只是一个示例,因此我可以理解执行异步文件 IO 的正确方法。关键是,在我从文件中读取大量数据或从慢速文件系统(例如 samba 共享、NFS)中读取数据的情况下,我不希望 UI 仅仅因为文件读取被阻塞而冻结.那么,如果不是线程,我该怎么做呢?
  • 你必须考虑 UI。如果文件非常大,您必须在 UI 和数据模型之间进行交互,这将为您提供用户当前看到的文件部分的信息,以便仅将该文件存储在内存中。这里的线程已经完全过时了。只是您试图以阻塞方式读取数据,这只是您尝试使用线程的原因。
  • 此外,文件不一定要很大才会成为问题。如果我尝试显示多达 1kb 的数据,我的示例应用程序就会锁定。这并不多。
  • 您的应用程序冻结,因为您按字符插入 1kb 数据。效率太低了。
  • 即使解决了这个问题,它的行为也完全一样。
【解决方案3】:

这似乎是您希望有一个带有信号量的消费者生产者的情况。有一个非常具体的example 可以引导您正确实施它。除了主线程之外,您还需要一个线程来完成这项工作。

设置应该是:

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

这几乎涵盖了将您从文件读取器读取的数据移动到您的小部件,但它不包括如何实际绘制这些数据。为了实现这一点,您可以通过覆盖 Hexviewer 的绘制事件并读取已放入队列的内容来使用绘制事件中的数据。更复杂的方法是写an event filter

除此之外,您可能希望读取最大字节数,然后明确指示 Hexviewer 使用数据。

请注意,此解决方案是完全异步的、线程安全的和有序的,因为您的任何数据都不会发送到 Hexviewer,但 Hexviewer 仅在需要在屏幕上显示时才使用这些数据。

【讨论】:

  • 所以我实际上尝试实现基于信号量的消费者/生产者设置,但无法使其工作(请参阅我的 repo @da1a1c7a)。问题是,生产者线程在尝试获取信号量时会阻塞,因此该线程中的事件循环无法处理更多事件。我还没有完全实现你所描述的(没有保存数据的第三类,它
  • @detly ,您应该定义两个信号量,如引用示例中所示,并有一个循环缓冲区来放置您的数据。使用一个信号量,您将阻塞,因为当生产者放置数据时,消费者无法使用它。我还会尝试使文件读取器无状态,并删除状态信息和关联的互斥锁。如果我没有提供太多帮助,请原谅。
  • @detly ,检查更新的答案,如果您解决了问题,请告诉我。
【解决方案4】:

对于十六进制查看器,我认为您根本没有走在正确的轨道上 - 除非您认为它很可能用于具有 SCSI 或 RAID 阵列的系统以提高速度。为什么要一次加载千兆字节的数据?如今,填充文本框的文件访问发生得非常快。当然,例如Notepad++ 有一个优秀的十六进制查看器插件,你必须先加载文件;但那是因为文件可能会被编辑,这就是 NPP 的工作方式。

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

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

如果您有多个线程读取一个文件,相比之下,您可能从开头读取一个,从中间读取另一个,最后读取另一个。单个读取头会四处跳动,试图满足所有请求,因此运行效率较低。如果它是一个 SDD 驱动器,非线性读取不会伤害你,但它们也不会帮助你。如果您希望权衡一下加载时间可能很明显,这样用户就可以随意滚动,更快一点(一个充满数据的文本框真正 毕竟加载并不需要很长时间)然后你可能有一个 single 线程在后台读取它,然后你可以让主线程继续处理事件循环。更简单的是,只需在一次打开整个文件时一次读取 n 兆字节的块,然后执行qApp-&gt;processEvents(); 以让 GUI 响应任何可能发生的 GUI 事件同时在每次读取块之后。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-30
    • 2019-04-23
    • 1970-01-01
    • 2015-10-09
    相关资源
    最近更新 更多