IT Notes

QFile и QThreadPool: Отзывчивое чтение файлов

Прочитать файл с диска в Qt предельно просто (без учета обработки ошибок):

static const QString FILE_NAME = …;
QFile file( FILE_NAME );
file.open( QIODevice::ReadOnly );
QByteArray data = file.readAll();

Но этот код работает приемлемо лишь для относительно небольших файлов. А что если размер файла переваливает за пару сотню Мб? В этом случае приведенная выше реализация заметно подвесит графический интерфейс пользователя. Исправить ситуацию нам помогут потоки.

Планирование

Воспользуемся реализацией потока в стиле QThreadPool на основе задачи QRunnable. Создадим простой класс LargeFileReaderTask, который умеет:

  1. Выполнять последовательное чтение файла блок за блоком с помощью QFile;
  2. Сообщать о ходе выполнения загрузки (сколько получено байтов от общего числа);
  3. Обеспечить возможность отмены чтения файла;
  4. Возвращать окончательный результат (прочитанные данные файла).

Реализация

Начинаем с интерфейса класса LargeFileReaderTask. Файл largefilereadertask.h:

#ifndef LARGEFILEREADERTASK_H
#define LARGEFILEREADERTASK_H

#include <QObject>
#include <QRunnable>
#include <QFile>
#include <QAtomicInt>

static const quint64 DEFAULT_CHUNK_SIZE_KB = 10000;
static const quint64 MIN_CHUNK_SIZE_KB = 1000;

class LargeFileReaderTask : public QObject, public QRunnable {
    Q_OBJECT
public:
    enum ResultCode {
        RESULT_OK,
        RESULT_FAILED,
        RESULT_CANCELLED
    };

public:
    LargeFileReaderTask( const QString& fileName, quint64 chunkSizeKb = DEFAULT_CHUNK_SIZE_KB );
    ~LargeFileReaderTask();

    void run();

public slots:
    void cancel();

signals:
    void progressChanged( int doneCount, int sumCount );
    void readingFinished( int resultCode, const QByteArray& data = QByteArray() );

private:
    QFile m_file;
    quint64 m_chunkSizeKb;

    QAtomicInt m_cancelledMarker;

};

#endif // LARGEFILEREADERTASK_H

Входными данными задачи являются имя файла и размер блока, который может быть прочитан за один шаг. Чем размер блока для чтения меньше, тем быстрее мы можем среагировать на запрос пользователя об остановке. Однако это добавляет накладные расходы и увеличивает суммарное время чтения. В качестве компромисса используем размер в 10 000 Кб.

Обратите внимание на поле m_cancelledMarker типа QAtomicInt. Оно понадобится нам для синхронизации действия отмены чтения. Подробнее об атомарных типах мы поговорим в другой раз.

Теперь реализация. Файл largefilereadertask.cpp:

#include "largefilereadertask.h"

LargeFileReaderTask::LargeFileReaderTask( const QString& fileName, quint64 chunkSizeKb ) :
    QObject( NULL ), m_file( fileName ), m_chunkSizeKb( chunkSizeKb ), m_cancelledMarker( false ) {

    if( m_chunkSizeKb < MIN_CHUNK_SIZE_KB ) {
        m_chunkSizeKb = MIN_CHUNK_SIZE_KB;
    }
}

LargeFileReaderTask::~LargeFileReaderTask() {
}

void LargeFileReaderTask::run() {
    if( !m_file.open( QIODevice::ReadOnly ) ) {
        emit readingFinished( RESULT_FAILED );
        return;
    }

    QByteArray data;
    QByteArray chunk;
    do {
        if( m_cancelledMarker.testAndSetAcquire( true, true ) ) {
            emit readingFinished( RESULT_CANCELLED );
            return;
        }

        try {
            chunk = m_file.read( m_chunkSizeKb * 1024 );
            data.append( chunk );
        } catch( … ) {
            emit readingFinished( RESULT_FAILED );
            return;
        }

        emit progressChanged( data.size(), m_file.size() );
    } while( !chunk.isEmpty() );

    if( m_file.error() != QFile::NoError ) {
        emit readingFinished( RESULT_FAILED );
        return;
    }

    emit readingFinished( RESULT_OK, data );
}

void LargeFileReaderTask::cancel() {
    m_cancelledMarker.fetchAndStoreAcquire( true );
}

Само чтение укладывается в функции run(), и заключается в строках:

chunk = m_file.read( m_chunkSizeKb * 1024 );
data.append( chunk );

Так мы делаем в цикле до тех пор, пока:

while( !chunk.isEmpty() );

Т.е. пока прочитанный блок не пустой.

Интеграция в GUI-приложение

С помощью LargeFileReaderTask можно создать подобное приложение:

large-file-reading-demo-gui-application

Оно позволяет:

  1. Выбрать и начать чтение любого файла по кнопке Open;
  2. Отображать ход выполнения операции чтения с помощью QProgressBar;
  3. Отменить чтение файла по кнопке Cancel.

Файл largefilereadingdemowidget.h:

#ifndef LARGEFILEREADINGDEMOWIDGET_H
#define LARGEFILEREADINGDEMOWIDGET_H

#include <QWidget>

namespace Ui {
class LargeFileReadingDemoWidget;
}

class LargeFileReadingDemoWidget : public QWidget {
    Q_OBJECT

public:
    explicit LargeFileReadingDemoWidget( QWidget* parent = 0 );
    ~LargeFileReadingDemoWidget();

signals:
    void cancelTaskRequired();

private slots:
    void onOpenFile();
    void onProgress( int doneCount, int sumCount );
    void onReadingFinished( int resultCode, const QByteArray& data );

private:
    Ui::LargeFileReadingDemoWidget* ui;
};

#endif // LARGEFILEREADINGDEMOWIDGET_H

И реализация из largefilereadingdemowidget.cpp:

#include "largefilereadingdemowidget.h"
#include "ui_largefilereadingdemowidget.h"

#include <QFileDialog>
#include <QThreadPool>
#include <QDebug>

#include "largefilereadertask.h"

LargeFileReadingDemoWidget::LargeFileReadingDemoWidget( QWidget* parent ) :
    QWidget( parent ), ui( new Ui::LargeFileReadingDemoWidget ) {
    ui->setupUi( this );

    connect( ui->bnOpen, SIGNAL( clicked( bool ) ), SLOT( onOpenFile() ) );

    ui->bnCancel->setDisabled( true );
}

LargeFileReadingDemoWidget::~LargeFileReadingDemoWidget() {
    emit cancelTaskRequired();

    delete ui;
    ui = NULL;
}

void LargeFileReadingDemoWidget::onOpenFile() {
    QString fileName = QFileDialog::getOpenFileName( this, "Open some large file" );
    if( fileName.isEmpty() ) {
        return;
    }

    LargeFileReaderTask* task = new LargeFileReaderTask( fileName );
    connect( task, SIGNAL( progressChanged( int, int ) ), SLOT( onProgress( int, int ) ) );
    connect( task, SIGNAL( readingFinished( int, QByteArray ) ), SLOT( onReadingFinished( int, QByteArray ) ) );

    connect( ui->bnCancel, SIGNAL( clicked( bool ) ), task, SLOT( cancel() ) );
    connect( this, SIGNAL( cancelTaskRequired() ), task, SLOT( cancel() ) );

    QThreadPool::globalInstance()->start( task );

    ui->bnCancel->setEnabled( true );
    ui->bnOpen->setDisabled( true );
}

void LargeFileReadingDemoWidget::onProgress( int doneCount, int sumCount ) {
    if( ui ) {
        ui->prgBar->setMaximum( sumCount );
        ui->prgBar->setValue( doneCount );
    }
}

void LargeFileReadingDemoWidget::onReadingFinished( int resultCode, const QByteArray& data ) {
    Q_UNUSED( data )

    if( ui ) {
        ui->bnCancel->setDisabled( true );
        ui->bnOpen->setEnabled( true );
    }

    switch ( resultCode ) {
    case LargeFileReaderTask::RESULT_OK: {
        // Успех
        qDebug() << "OK";
        break;
    }

    case LargeFileReaderTask::RESULT_FAILED:
        // Неудача
        qDebug() << "FAILED";
        break;

    case LargeFileReaderTask::RESULT_CANCELLED:
        // Чтение отменено
        qDebug() << "CANCELLED";
        break;

    default:
        break;
    }
}

Замечание

Не забывайте, что размер оперативной памяти ограничен. А также имеется ее фрагментация. Поэтому непрерывные блоки больших размеров может быть невозможно уместить в RAM в той или иной ситуации. В связи с этим слишком большие файлы в память грузить не рекомендуется. Лучше обрабатывать их поточными алгоритмами "на лету".

Исходники

 Скачать исходники с примером отзывчивого чтения файлов в Qt с помощью QFile и QThreadPool

Похожие публикации

Комментарии

Идея интересная. Но оно падает с ошибкой сегментирование при попытке чтения даже небольшого файла в 3.6 гигабайт. На линуске.

Позже посмотрю, почему. А пока, увы.

Anonymous:

Идея интересная. Но оно падает с ошибкой сегментирование при попытке чтения даже небольшого файла в 3.6 гигабайт. На линуске.

Позже посмотрю, почему. А пока, увы.

Здравствуйте. Размер в 3.6 Гб не такой уж и небольшой :)

Для чтения файлов больше пары сотен Мб описанный в статье подход лучше не использовать. Вероятно, проблема связана с принципом работы функции append() из QByteArray. Сначала выделяется непрерывный буфер некоторого размера. Если после append-а размер буфера превышен, то выделяется еще один непрерывный буфер большего размера (скорее всего, в два раза больше предыдущего), в который копируется содержимое старого. В худшем случае при чтении файла в 3.6 Гб в тот или иной может потребоваться до 8 Гб оперативной памяти. Все осложняется условием непрерывности буферов.

Можно резервировать место для буфера заранее (через reserve()). Это решение является в данной ситуации предпочтительным.

Ну. если все пытаться хранить в QByteArray, то там ограничение будет примерно 2 в 31 степени байт. Соответственно, будет крах. Лучше такое обрабатывать, но это все таки учебный материал. :)

А большие файлы в моем понимании > 20 гигов. На таких я сей подход даже пробовать не стал. Хотя мог бы, конечно. Просто по работе такого очень много.

Доброго дня. А есть ли реализация подхода типа виндовой функции CreateFileMapping?

Anonymous:

Доброго дня. А есть ли реализация подхода типа виндовой функции CreateFileMapping?

Здравствуйте. Да, в Qt предусмотрена реализация отображения файлов в память. В Qt 4.x для этого предназначена функция-член QFile::map(), а в Qt 5.x - QFileDevice::map()