Длительные операции в программе можно
разделить на две категории. Первая — интенсивные вычислительные задачи,
такие как: алгоритмы обработки аудио и видео данных, моделирование.
Вторая — взаимодействие с медленными объектами «внешнего мира»
программы(управление различным промышленным оборудованием, загрузка
файлов по сети и проч.). Для второй категории задач проблема сохранения
отзывчивости решается в Qt использованием асинхронных классов, т.е.
классов, методы которых сразу после вызова возвращают управление, а о
результате своей деятельности они оповещают соответствующим сигналом.
Примером таких классов могут служить классы QFtp и QHttp.
В случае интенсивных вычислений (в не зависимости от использования
механизма сигнал-слот) обработка событий прерывается, сетевое
взаимодействие останавливается, таймеры замирают, программа не
перерисовывается и перестает отвечать на ввод пользователя. Иными
словами до окончания вычислений программа выглядит зависшей. Сохранить
отзывчивость программы можно либо периодической обработкой ожидающих в
очереди событий и тогда необходимые вычисления можно оставить в главном
потоке, либо созданием дополнительного потока и переносом вычислительных
алгоритмов в него. Создание многопоточных приложений требует высокой
квалификации от программиста и несколько усложняет код. Для тех, кого не
пугает усложнение исходного кода считаю необходимым напомнить первый
закон творческого программирования: «Caveat emptor: стоимость
сопровождения программного обеспечения пропорциональна квадрату
творческих способностей программиста». Выбор пути решения зависит от
задачи. Практически любую вычислительную задачу можно разбить на
несколько подзадач, на несколько частей. Если эти части могут быть
решены только последовательно, то велика вероятность того, что
оптимальным вариантом будут вычисления в главном потоке. Ежели отдельные
части независимы и могут вычисляться параллельно, то использование
нескольких потоков может дать значительный прирост производительности.
Самое простое решение заключается в периодическом вызове QCoreApplication::processEvents()
в коде вычислений. Эта функция обрабатывает ожидающие в очереди
события. В случае, если действия пользователя нежелательны во время
выполнения задачи можно отключить обработку событий мыши и клавиатуры
заменив вызов QCoreApplication::processEvents() на QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents). С помощью QCoreApplication::processEvents()
также можно «синхронизировать» асинхронные методы. Допустим у нас есть
класс для общения с неким устройством. Ему можно послать команду с
помощью метода send(QByteArray array) и получить ответ в слот data(QByteArray array)
в течение некоторого времени. На случай отсутствия ответа предусмотрим
таймаут. Если ответ не придет в течение 5 секунд, то ожидание мы
прекращаем.
MyClass::say(QByteArray array) { QEventLoop::ProcessEventsFlags flags; ready = false; timeout = false; flags |= QeventLoop::WaitForMoreEvents; myTimer.start(5000); send(array); while ((ready == false) && (timeout == false)) { QCoreApplication::processEvents(flags); } myTimer.stop; if (ready == true) { } else { } } MyClass::timeout() { timeout = true; } MyClass::data(QByteArray array) { ready = true; }
Если существует возможность разбить задачу на куски, то можно
воспользоваться 0-миллисекундным таймером. Такой таймер срабатывает
когда в очереди нет событий и приложение простаивает:
void MyClass::timeIsCome() { while ((part < partsNumber) && (!qApp->hasPendingEvents())) { doPart(part); ++part; } }
В цикле выполняем части задачи пока не выполним все или пока не
появятся ожидающие в очереди события. Альтернативой таймеру может
служить метод QMetaObject::invokeMethod(). Для одного потока
по-умолчанию используется прямой вызов, таким образом необходимо вызвать
метод с явным указанием типа подключения Qt::QueuedConnection: QMetaObject::invokeMethod(this, "doPart", Qt::QueuedConnection).
Подход аналогичен способу с таймер за тем исключением, что в слот мы
можем передать аргумент. Недостаток этих подходов заключается в
отсутствии информации когда и сколько приложение будет в состоянии
ожидания.
Использование нескольких потоков — еще один вариант сохранения
отзывчивости программы. Qt конечно же предоставляет поддержку потоков в
виде кроссплатформенных потоковых классов. Поддерживается
потокобезопасный способ отправки событий, между потоками возможно
установка соединения сигнал-слот. Доступны низкоуровневые потоковые
примитивы: мьютексы, блокировки чтение-запись, семафоры. В дополнении к
этому в Qt 4.4 было добавлено пространство имен QtConcurrent содержащее
высокоуровневое API для многопоточного программирования, которое
включает управление асинхронными вычислениями и параллельную обработку
списков. QtConcurrent берет на себя заботу о создании дополнительных
потоков количество которых зависит от количества процессоров в системе
на которой запускается приложение (см. QThread.idealThreadCount()).
Основные функции QtConcurrent :
* QtConcurrent::map() применяет функцию к каждому элементу контейнера, модифицируя элементы этого контейнера.
* QtConcurrent::mapped() подобна map(), но возвращает новый контейнер с измененными элементами.
* QtConcurrent::mappedReduced() подобна mapped(), но элементы сворачиваются в один результат.
* QtConcurrent::filter() Основываясь на результате
функции-фильтра удаляет элементы контейнера. Если функция-фильтр
возвращает true, то элемент остается в последовательности, иначе
удаляется.
* QtConcurrent::filtered() подобна filter(), но отфильтрованный результат возвращается в новом контейнере.
* QtConcurrent::filteredReduced() подобна filtered(), но отфильтрованные элементы сворачиваются в один результат.
* QtConcurrent::run() запускает функцию в другом потоке.
* QFuture представляет результат асинхронного вычисления.
* QFutureIterator позволяет перемещаться по результатам доступным через QFuture.
* QFutureWatcher позволяет отслеживать QFuture через механизм сигнал-слот.
* QFutureSynchronizer класс для синхронизации нескольких QFuture.
Удобство использования QtConcurrent продемонстрируем на примере
многопоточного экспорта html файлов в формат ODT (ISO/IEC 26300:2006
Open Document Format for Office Applications). В диалоге выбора файлов
выбираем html файлы и в несколько потоков экспортируем их в Open
Document Format. Процесс экспорта в любой момент можно приостановить или
прервать. Сохранение в формат odt производит с помощью класса QTextDocumentWriter.
Листинг html2odf.h:
#ifndef WINDOW_H #define WINDOW_H #include <QtGui> class Window : public QWidget { Q_OBJECT public: Window(QWidget *parent = 0); ~Window(); public Q_SLOTS: void open(); void showProgress(int num); void savingResult(int index); void finished(); private: QStringList *files; QPushButton *openButton; QPushButton *cancelButton; QPushButton *pauseButton; QVBoxLayout *mainLayout; QProgressBar *progress; QHBoxLayout *progressLayout; QFutureWatcher<bool> *odfSaving; }; #endif
Листинг html2odf.cpp:
#include "html2odf.h" bool save(const QString &fileName) { if (!QFile::exists(fileName)) return false; QFile file(fileName); if (!file.open(QFile::ReadOnly)) return false; QByteArray data = file.readAll(); QTextCodec *codec = Qt::codecForHtml(data); QString str = codec->toUnicode(data); if (Qt::mightBeRichText(str)) { QTextDocument d; d.setHtml(str); QTextDocumentWriter writer(fileName + ".odt", "odf"); return writer.write(&d); } else { return false; } } Window::Window(QWidget *parent) : QWidget(parent) { setWindowTitle(tr("html2odf")); files = new QStringList(); odfSaving = new QFutureWatcher<bool>(this); connect(odfSaving, SIGNAL(progressValueChanged(int)), SLOT(showProgress(int))); connect(odfSaving, SIGNAL(resultReadyAt(int)), SLOT(savingResult(int))); connect(odfSaving, SIGNAL(finished()), SLOT(finished())); openButton = new QPushButton(tr("Select HTML files")); connect(openButton, SIGNAL(clicked()), SLOT(open())); cancelButton = new QPushButton(tr("Cancel")); cancelButton->setEnabled(false); connect(cancelButton, SIGNAL(clicked()), odfSaving, SLOT(cancel())); pauseButton = new QPushButton(tr("Pause/Resume")); pauseButton->setEnabled(false); connect(pauseButton, SIGNAL(clicked()), odfSaving, SLOT(togglePaused())); QHBoxLayout *buttonLayout = new QHBoxLayout(); buttonLayout->addWidget(openButton); buttonLayout->addWidget(cancelButton); buttonLayout->addWidget(pauseButton); buttonLayout->addStretch(); progressLayout = new QHBoxLayout(); progress = new QProgressBar(); progressLayout->addWidget(progress); mainLayout = new QVBoxLayout(); mainLayout->addLayout(buttonLayout); mainLayout->addLayout(progressLayout); mainLayout->addStretch(); setLayout(mainLayout); } Window::~Window() { odfSaving->cancel(); odfSaving->waitForFinished(); } void Window::open() { if (odfSaving->isRunning()) { odfSaving->cancel(); odfSaving->waitForFinished(); } *files = QFileDialog::getOpenFileNames(this, tr("Select HTML files"), QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation), "*.htm *.html"); if (!files->isEmpty()) { odfSaving->setFuture(QtConcurrent::mapped(*files, save)); progress->setMinimum(odfSaving->progressMinimum()); progress->setMaximum(odfSaving->progressMaximum()); openButton->setEnabled(false); cancelButton->setEnabled(true); pauseButton->setEnabled(true); } } void Window::showProgress(int num) { progress->setValue(num); } void Window::savingResult(int index) { qDebug() << files->at(index) + ".odt" << " saved: " << odfSaving->resultAt(index) ; } void Window::finished() { openButton->setEnabled(true); cancelButton->setEnabled(false); pauseButton->setEnabled(false); }
Источник: http://www.qtinfo.ru/stayingresponsive |