我使用Qt 5.2.1来实现从文件中读取数据的程序(可能是几个字节到几GB),并以一种依赖于每个数据的方式显示数据字节。我的例子是一个十六进制查看器。
一个对象执行读取操作,并在读取新数据块时发出信号dataRead()
。信号带有指向QByteArray
的指针,如下所示:
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
插槽连接到此信号,这是显示数据的功能:
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:我是否自己实施线程等?我不是重新发明轮子的忠实粉丝,尤其关于内存管理和线程。是否有更高级别的构造可以做到这一点,就像网络传输一样?
答案 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可以指导您正确实施它。你还需要一个线程来使这个工作与你的主线程分开。
设置应该是:
QSemaphore::acquire()
之前,我需要检查QSemaphore :: available()`以确保,以避免阻止GUI。这几乎涵盖了将数据读取从文件读取器移动到窗口小部件,但它没有涵盖如何实际绘制此数据。为了实现这一目标,您可以通过覆盖Hexviewer的绘制事件并读取已放入队列中的内容来消耗痛苦的数据。更精细的方法是编写an event filter。
除此之外,您可能希望读取最大字节数,然后显式信号通知Hexviewer使用数据。
请注意,此解决方案完全异步,线程安全且有序,因为您的数据都没有发送到Hexviewer,但Hexviewer只在需要在屏幕上显示时消耗它。
答案 2 :(得分:0)
如果您计划编辑忘记QTextEdit
的10GB文件。这个ui->hexTextView->insertPlainText
只会占用整个内存,然后才能读取文件的1/10。 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();
}
它比我预期的要短,需要一些改进,但它应该是一个很好的基础。
答案 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()来保存结果,以便它可以正确返回结果。您需要支付它阻塞向量的价格才能返回其结果;但实际上,实际开销似乎并不多。在这种情况下,共享内存可能也是值得追求的-但也许每个线程都可以拥有自己的内存,因此您无需锁定从其他线程结果中读取的内容来编写它。