如何在Qt中异步加载大文件中的数据
How can I asynchronously load data from large files in Qt?
我使用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之前发出。
(完整的代码可以运行,当你使用大于1kB的文件时,你会看到这种行为。)
根据对我论坛帖子Qt5中非阻塞本地文件IO的回应,以及对另一个堆栈溢出问题的回答如何在qt中执行异步文件IO?,答案是:使用线程。但是,这些答案都没有详细说明如何打乱数据本身,也没有说明如何避免常见的错误和陷阱。
如果数据很小(大约一百个字节),我会把它和信号一起发射出去。但是,如果文件大小为GB(edit),或者文件位于基于网络的文件系统上,例如NFS、Samba共享,我不希望UI仅仅因为读取文件块而锁定。
第二个问题是,在发射器中使用new
,在接收器中使用delete
的机制似乎有点天真:我有效地将整个堆用作跨线程队列。
问题1:Qt是否有更好/惯用的方法在限制内存消耗的同时跨线程移动数据?它是否有线程安全队列或其他可以简化整个过程的结构?
问题2:我是否有自己实现线程等?我不太喜欢重新发明轮子,尤其是在内存管理和线程方面。是否有更高级别的构建已经可以做到这一点,就像网络传输一样?
首先,您的应用程序中根本没有任何多线程。您的FileReader
类是QThread
的子类,但这并不意味着所有FileReader
方法都将在另一个线程中执行。事实上,所有操作都是在主(GUI)线程中执行的。
FileReader
应该是QObject
而不是QThread
子类。然后创建一个基本的QThread
对象,并使用QObject::moveToThread
将工作人员(读取器)移动到该对象。你可以在这里阅读有关这项技术的内容。
请确保您已使用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
使用隐式共享。这意味着在只读模式下跨函数传递QByteArray
对象时,其内容不会被一次又一次地复制。
将上面的代码更改为这个,忘记手动内存管理:
while(!inFile.atEnd())
{
QByteArray qa = inFile.read(DATA_SIZE);
qDebug() << "emitting dataRead()";
emit dataRead(qa);
}
但无论如何,主要问题不在于多线程。问题是QTextEdit::insertPlainText
操作并不便宜,尤其是当您有大量数据时。FileReader
非常快速地读取文件数据,然后用要显示的新数据部分淹没小部件。
必须注意的是,您对HexViewer::loadData
的实现非常无效。您一个字符接一个字符地插入文本数据,这使得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小部件,您无法轻松显示大于几兆字节的巨大文本。此任务需要一些非琐碎的方法,这些方法通常与多线程或异步数据加载无关。这一切都是为了创建一些棘手的小部件,它不会试图同时显示其庞大的内容。
这似乎是您想要一个具有信号量的消费者-生产者的情况。有一个非常具体的例子可以引导你正确地实现它。你需要一个额外的线程来使它与主线程分开工作。
设置应为:
- 线程A以生产者身份运行文件读取器
- GUI线程运行Hexviewer小部件,该小部件消耗特定事件的数据在发出
QSemaphore::acquire()
之前,应使用QSemahore::available()`进行检查,以避免阻塞GUI - Filereader和Hexviewer可以访问第三类,例如DataClass,其中数据在读取时放置,并从消费者处检索。这也应该定义信号量
- 不需要发出带有数据的信号或通知
这几乎涵盖了将从filereader读取的数据移动到小部件,但并不涵盖如何实际绘制这些数据。为了实现这一点,您可以通过覆盖Hexviewer的绘制事件并读取队列中的内容来使用paintevent中的数据。更详细的方法是编写一个事件过滤器。
除此之外,您可能希望读取最大字节数,然后显式地用信号通知Hexviewer使用数据。
请注意,这个解决方案是完全异步的、线程安全的和有序的,因为没有任何数据发送到Hexviewer,但Hexviewers只在需要在屏幕上显示时使用这些数据。
-
如果您计划编辑10GB文件,请忘记
QTextEdit
。在您读取文件的1/10之前,这个ui->hexTextView->insertPlainText
只会吃掉整个内存。IMO您应该使用QTableView
来显示和编辑数据。为此,您应该继承QAbstractTableModel
。在一行中,您应该显示16个字节。前16列为十六进制,下一列为ASCII。这应该不会太复杂。只是可怕地阅读QAbstractTableModel
的文档。缓存数据在这里将是最重要的。如果我有时间,我会给出代码示例。 -
忘记使用多线程。使用这样的东西是不好的,很可能会产生很多与同步相关的问题。
好的,我花了一些时间,这里是正在工作的代码(我已经测试过它工作得很顺利):
#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();
}
它比我预期的要短,需要一些改进,但它应该是一个很好的基础。
对于十六进制查看器,我认为您根本没有走在正确的轨道上——除非您认为它很可能用于带有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:你没有做错。我必须对结果结构执行Q_DECLARE_METATYPE,为其定义构造函数、析构函数和移动运算符(我使用了memmove),并对结构和向量执行qRegisterMetaType()以保存结果,使其正确返回结果。为了返回结果,你付出了它阻塞向量的代价;但实际开销似乎根本不算多。在这种情况下,共享内存可能也值得追求,但也许每个线程都有自己的,所以你不需要锁定其他线程结果的读取来写入它
- 从 bmp 文件数据创建 MFC CBitmap
- 从存储为 Windows 资源 (c++) 的 png 中获取 png 文件数据
- 如何在 Gnuplot 中分别绘制 2 个文件数据?我有一个文件"sin.txt",另一个文件"cos.txt",我想将它们分别绘制在一个图表上
- 获取文件数据预处理器宏
- 错误的值是使用c++从CSV文件数据写入数组
- 无法在可视C++中检索资源文件数据
- 将文本文件数据读入字符数组时提取运算符的歧义
- 如何使用curl c ++源代码发布wav文件数据,如何使用c ++使用--data-binary?
- 如何在C++中读取 UTF-8 文件数据
- 如何在结构中存储二进制文件数据
- 无法从 turbo c 程序中的文件检索文件数据
- 使用 fstream 将文件数据从当前位置保存到文件末尾
- 通过Google Protobuf发送二进制文件数据
- 在通过套接字向服务器发送文件数据时,我得到了一些垃圾值?为什么
- 如何使用dlang读取二进制文件数据
- 程序使用FSCANF读取文件数据后崩溃
- 检测二进制文件数据的端性
- C++ 从文本文件数据类型读取为结构,并将数据存储在列表的向量中
- 将 FFT 应用于 WAV 文件数据的 C++
- C++内存映射的文件数据预取