QFile::flush()和QFile::close()的区别

3
我正在实现一个方法,该方法将从TableView对象中写出数据到CSV文件。然而,当程序运行时,程序以非常缓慢的速度(3或4秒)将数据写入USB驱动器上的文件,但在系统内部驱动器上正常工作。这是因为我在写完文件后没有使用flush()或close()吗?
以下是我的代码:
bool ThicknessCalibrationDataDisplay::WriteCSVFileChanges()
{
    QModelIndex tableViewModelindex =  tableViewModel_->index(0,0);

    QFile file(CSVFileName_);
    if(!file.exists())
        return false;

    if(!file.open(QIODevice::WriteOnly))
        return false;


    for(int i = 0; i < totalRows_ ; i++)
    {
        for(int j = 0 ; j < totalColumns_; j++)
        {
            tableViewModelindex =  tableViewModel_->index(i,j);
            qDebug()<<tableViewModelindex.data();
            QString text = tableViewModelindex.data().toString();
            QTextStream OutputStream(&file);

            if(j == totalColumns_ - 1)
                OutputStream<<"\n\r";
            else
                OutputStream<<',';

        }
    }

}

这是我以前的代码,现在我计划关闭文件流,以实现优雅退出。 QFile :: close()的Qt API说:

调用QFile :: flush()并关闭文件。忽略flush的错误。

我只需调用close(),还是最好调用flush(),记录任何错误,然后调用close()?
是否有其他修改可以进行,以改进写操作?

那听起来像是糟糕的设计,忽略错误。如果真的是这样发生的话,那么是的,你必须先调用flush并处理错误。(但在任何合理的设计中,close应该将从flush上传递的任何错误传播上去。) - James Kanze
真的。我很惊讶close()不会返回任何布尔错误状态... - Barath Ravikumar
只是好奇,QIODevice或其他东西相比标准流有什么优势。 - James Kanze
在整个代码库中,使用了Qt文件流类,因此只需执行相同操作以维护项目完整性... - Barath Ravikumar
如果你不是独自工作,那么是的,你必须在项目中保持某种连贯性。(尽管在IO的情况下,根据要求使用几种不同的解决方案并不罕见。事务处理和日志记录的要求截然不同,满足其中一个的io子系统很少能满足另一个。但从你展示的内容来看,我认为没有理由打破应用程序中现有的惯例。) - James Kanze
2个回答

8
  1. The flush() vs. close() is a red herring, it doesn't impact your performance at all.

  2. Destruction of a QTextStream forces a flush of the file. Flushing the file is slow. You're destroying your text stream every time you iterate through the loop! Create the stream outside of the loop!

    Here's the source, from Qt 5.1.1:

    QTextStream::~QTextStream()
    {
        if (!d->writeBuffer.isEmpty())
            d->flushWriteBuffer();
    }
    

    On Qt 5.1 or newer, you should be using QSaveFile if you want to ensure that the disk buffers are flushed once the file is closed. Only this gives you assurance that once you think you're done saving, the data is in fact on the disk.

  3. QTextStream has its own buffers. So, if you want to catch errors while flushing, you need to use the flush() method on the stream itself, not on the QFile.

  4. It's a very bad idea to do any file access from the GUI thread. Any time anything blocks, your UI thread blocks as well.

    Now the question is: how do you make your model safe to access from another thread? If you use a custom model, you probably only need to switch the model into a read-only mode for the duration of writing the file. The read-only mode would be a custom property of the model, such that all setData calls would fail. You'd of course need to indicate this to the user. The views of the model would be read-only, but that's much more friendly than blocking the entire GUI.

    If you think that merely preventing concurrent access to the model with a QMutex would be enough, think again. Any modifications made to the model may potentially change its structure, so your writer would need to properly handle all signals emitted by model. This would make the writer much more complex. A temporarily read-only model lets you have responsive GUI with a temporary inconvenience to the user, at a minimal increase in code complexity.

    class MyModel : public QStandardItemModel /*  or whatever other class you derive from */ {
        typedef QStandardItemModel inherited;
        Q_OBJECT
        Q_PROPERTY(bool writable READ isWritable WRITE setWritable NOTIFY writableChanged)
        bool m_writable;
    public:
        explicit MyModel(QObject * parent) : inherited(parent), m_writable(true) {}
        Q_SLOT void setReadOnly() {
            if (!m_writable) return;
            m_writable = false; 
            emit writableChanged(m_writable);
        }
        Q_SLOT void setReadWrite(){
            if (m_writable) return;
            m_writable = true; 
            emit writableChanged(m_writable);
        }
        Q_SIGNAL void writableChanged(bool);
        bool isWritable() const { return m_writable; }
        void setWritable(bool writable) {
           if (m_writable == writable) return;
            m_writable = writable; 
            emit writableChanged(m_writable);
        }
        bool setData(const QModelIndex & index, const QVariant & val, int role = Qt::EditRole) {
            if (! m_writable) return false;
            return inherited::setData(index, val, role);
        }
    };
    
  5. Some USB drives are abysmally slow.

您重新编写的代码应该至少如下所示:

bool ThicknessCalibrationDataDisplay::WriteCSVFileChanges()
{
#if QT_VERSION < QT_VERSION_CHECK(5,1,0)
    QFile file(CSVFileName_);
#else
    QSaveFile file(CSVFileName_); // does a disk commit at the end
#endif
    if(!file.exists())
        return false;

    if(!file.open(QIODevice::WriteOnly))
        return false;

    QTextStream os(&file);
    for(int i = 0; i < totalRows_ ; i++)
    {
        for(int j = 0 ; j < totalColumns_; j++)
        {
            QModelIndex index = tableViewModel_->index(i,j);
            qDebug() << index.data();
            os << index.data();
            if(j == totalColumns_ - 1)
                os<<"\n\r";
            else
                os<<',';

        }
    }
    os.flush();
#if QT_VERSION >= QT_VERSION_CHECK(5,1,0)
    return os.status() == QTextStream::Ok && file.commit();
#else
    return os.status() == QTextStream::Ok;
#endif
}

谢谢,我会根据第二点重新设计我的代码,这是你提出的一个非常好的观点。但我不确定是否应该使模型只读,因为我可以接受GUI锁定,因为它是整个系统中唯一设计运行的程序(类似便携式PDA设备)... - Barath Ravikumar
非常感谢,再多也没法要求更多了..!! - Barath Ravikumar
关于第一点:它不会影响性能,但会影响纠正。在某个时刻,您需要知道所有通过最终刷新的操作是否成功(或失败)。如果关闭未返回错误状态,则需要在关闭之前进行显式刷新。 - James Kanze
关于第四点,我基本上同意,但也有例外;在UI线程中写入一两行代码是完全可以接受的(包括刷新,前提是文件未在系统级别上同步)。你只需要等待数据被复制到系统缓冲区即可,这相当快。否则你怎么实现日志记录呢? - James Kanze
@JamesKanze:记录通常使用无锁队列完成,文件访问在单独的线程中完成。就你而言,用户在进行操作时可能会突然拔出(或碰撞)USB驱动器,导致应用程序无故冻结。我讨厌这种情况,你也是,其他人也是。 - Kuba hasn't forgotten Monica
@KubaOber 除非你只是写一两行代码,否则它没有影响,因为操作系统会进行缓冲。只有当你开始溢出操作系统的缓冲区(或正在进行事务处理并需要完全同步的写入)时,这些问题才会产生影响。 - James Kanze

8

我可以告诉你,在函数末尾会隐式调用QFile::close(),因为在文件变量的作用域结束时,QFile对象的析构函数也会被调用。根据QFile文档:

QFile::~QFile ()
Destroys the file object, closing it if necessary.

您可能想要尝试的是操作系统中的底层同步功能。据我所知,Qt没有为此提供任何API,因此无法实现可移植性。当调用QFile::flush()函数时,可能发生的情况(至少在Linux上)是从用户空间缓冲区刷新数据到底层系统缓存,而不一定是写入磁盘。在linux/unix上,您需要使用fsync函数来确保文件实际上被写入磁盘。代码应该如下:

file.flush();
fsync( file.handle() );
file.close();

我相信相同的代码也适用于Windows系统。
有关fflush与fsync更多信息,请查看fflush和fsync之间的区别

5
在Unix上,QFSFileEnginePrivate::nativeSyncToDisk() 函数使用 fsync() 实现同步到磁盘。在Windows上,它使用 FlushFileBuffers(handle)。该方法被用于 QSaveFile 中。在Qt 5.1中应该使用这个方法。 - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接