В недавнем проекте с Qt пришлось разбираться с классом QThread. В результате вышел на «правильную» технологию работы c QThread, которую буду использовать в других проектах.
Задача
Есть служба, которая следит за каталогом с довольно большим, от сотен до тысяч, количеством файлов. На основе анализа содержимого файлов строятся отчеты (до пяти отчетов разных видов). Если содержимое какого-либо файла меняется, либо меняется количество файлов, построение отчетов прерывается и начинается заново. Функциональность построения отчета реализована в классе ReportBuilder
. Для каждого вида отчетов используется свой класс, наследуемый от ReportBuilder
. Отчеты желательно строить в параллельных потоках (threads).
Примеры в документации Qt: неправильно
Начал с чтения документации и примеров Qt. Во всех примерах поток создается наследованием класса QThread и переопределением метода run()
:
class MyThread : public QThread
{
Q_OBJECT
protected:
void run();
};
void MyThread::run()
{
...
}
По ходу прочитал пост, в котором разработчик Qt Bradley T.Hughes утверждает, что наследование QThread только для выполнения кода класса в отдельном потоке – идея в корне неправильная:
«QThread was designed and is intended to be used as an interface or a control point to an operating system thread, not as a place to put code that you want to run in a thread. We object-oriented programmers subclass because we want to extend or specialize the base class functionality. The only valid reasons I can think of for subclassing QThread is to add functionality that QThread doesn’t have, e.g. perhaps providing a pointer to memory to use as the thread’s stack, or possibly adding real-time interfaces/support. Code to download a file, or to query a database, or to do any other kind of processing should not be added to a subclass of QThread; it should be encapsulated in an object of it’s own.»
«Класс QThread создан и предназначен для использования в качестве интерфейса к потокам операционной системы, но не для того, чтобы помещать в него код, предназначенный для выполнения в отдельном потоке. В ООП мы наследуем класс для того чтобы расширить или углубить функциональность базового класса. Единственное оправдание для наследования QThread, которое я могу представить, это добавление такой функциональности, которой в QThread не существует, например, передача указателя на область памяти, которую поток может использовать для своего стека, или, возможно, добавление поддержки интерфейсов реального времени. Загрузка файлов, работа с базами данных, и подобные функции не должны присутствовать в наследуемых классах QThread; они должны реализовываться в других объектах»
Т.е. наследование от QThread не то чтобы совсем неправильно, но приводит к ненужному смешиванию разных наборов функций в одном классе, что ухудшает читаемость и поддерживаемость кода. Но, если наследовать QThread неправильно, то как тогда правильно? После небольшого поиска, нашел вот этот пост, в котором все разложено по полочкам. Ключевые моменты поста:
- QThread – это не поток, а Qt обертка для потока конкретной ОС, которая позволяет взаимодействовать с потоком из Qt проекта, в первую очередь через Qt signals/slots.
- Выделение памяти оператором new экземплярам класса, предназначенным для выполнения в отдельном потоке должно осуществляться уже в потоке. Собственником объекта будет тот поток, который выделил объекту память.
- Для управления потоками и «живущими» в них объектами важно правильно настроить обмен сообщениями.
Как правильно
Итак «правильный» рецепт запуска и остановки классов в потоках:
Создаем обертку для класса, который будет жить в отдельном потоке. В нашем случае это ReportBuilder
. Обертка для него: RBWorker
.
class RBWorker : public QObject {
Q_OBJECT
private:
ReportBuilder *rb; /* построитель отчетов */
QStringList file_list; /* список файлов для обработки */
ReportType r_type; /* тип отчета */
public:
RBWorker(ReportType p_type );
~RBWorker();
void setFileList(const QStringList &files) { file_list = files; } /* передача списка файлов для обработки */
public slots:
void process(); /* создает и запускает построитель отчетов */
void stop(); /* останавливает построитель отчетов */
signals:
void finished(); /* сигнал о завершении работы построителя отчетов */
};
RBWorker:: RBWorker (ReportType p_type)
{
rb = NULL;
r_type = p_type;
}
RBWorker::~ RBWorker ()
{
if (rb != NULL) {
delete rb;
}
}
void RBWorker::process()
{
if(file_list.count() == 0) {
emit finished();
return;
}
switch (r_type) {
case REPORT_A: {
rb = new ReportBuilderA ();
break;
}
case REPORT_B: {
rb = new ReportBuilderB ();
break;
}
case REPORT_C: {
rb = new ReportBuilderC ();
break;
}
default:
emit finished();
return ;
}
}
rb->buildToFile(file_list); /* выполнение buildToFile прерывается вызовом rb->stop() */
emit finished();
return ;
}
void RBWorker::stop() {
if(rb != NULL) {
rb->stop();
}
return ;
}
Важный момент: экземпляр ReportBuilder
создается в методе process(),
а не в конструкторе RBWorker
.
Класс Session
отслеживает изменения в файлах и запускает построение отчетов
class Session : public QObject {
Q_OBJECT
public:
Session(QObject *parent, const QString &directory, const QVector<ReportType> &p_rt);
~Session();
void buildReports();
private:
void addThread(ReportType r_type);
void stopThreads();
QStringList files;
QVector<ReportType> reports; //виды отчетов
signals:
void stopAll(); //остановка всех потоков
};
Самый важный метод в классе: addThread
void Session::addThread(ReportType r_type)
{
RBWorker* worker = new RBWorker(r_type);
QThread* thread = new QThread;
worker->setFileList(files); /* передаем список файлов для обработки */
worker->moveToThread(thread);
/* Теперь внимательно следите за руками. Раз: */
connect(thread, SIGNAL(started()), worker, SLOT(process()));
/* … и при запуске потока будет вызван метод process(), который создаст построитель отчетов, который будет работать в новом потоке
Два: */
connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
/* … и при завершении работы построителя отчетов, обертка построителя передаст потоку сигнал finished() , вызвав срабатывание слота quit()
Три:
*/
connect(this, SIGNAL(stopAll()), worker, SLOT(stop()));
/* … и Session может отправить сигнал о срочном завершении работы обертке построителя, а она уже остановит построитель и направит сигнал finished() потоку
Четыре: */
connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
/* … и обертка пометит себя для удаления при окончании построения отчета
Пять: */
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
/* … и поток пометит себя для удаления, по окончании построения отчета. Удаление будет произведено только после полной остановки потока.
И наконец :
*/
thread->start();
/* Запускаем поток, он запускает RBWorker::process(), который создает ReportBuilder и запускает построение отчета */
return ;
}
void Session::stopThreads() /* принудительная остановка всех потоков */
{
emit stopAll();
/* каждый RBWorker получит сигнал остановиться, остановит свой построитель отчетов и вызовет слот quit() своего потока */
}
void Session::buildReports()
{
stopThreads();
for(int i =0; i < reports.size(); ++i) {
addThread(reports.at(i));
}
return ;
}
void Session::~Session()
{
stopThreads(); /* останавливаем и удаляем потоки при окончании работы сессии */
…
}
На практике все заработало практически сразу. Большое спасибо Maya Posch – помогла разобраться.
Автор: tba