В недавнем проекте с Qt пришлось разбираться с классом QThread. В результате вышел на «правильную» технологию работы c QThread, которую буду использовать в других проектах.
Есть служба, которая следит за каталогом с довольно большим, от сотен до тысяч, количеством файлов. На основе анализа содержимого файлов строятся отчеты (до пяти отчетов разных видов). Если содержимое какого-либо файла меняется, либо меняется количество файлов, построение отчетов прерывается и начинается заново. Функциональность построения отчета реализована в классе
Начал с чтения документации и примеров Qt. Во всех примерах поток создается наследованием класса QThread и переопределением метода
По ходу прочитал пост, в котором разработчик 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 неправильно, то как тогда правильно? После небольшого поиска, нашел вот этот пост, в котором все разложено по полочкам. Ключевые моменты поста:
Итак «правильный» рецепт запуска и остановки классов в потоках:
Создаем обертку для класса, который будет жить в отдельном потоке. В нашем случае это
Важный момент: экземпляр
Класс
Самый важный метод в классе:
На практике все заработало практически сразу. Большое спасибо Maya Posch – помогла разобраться.
Задача
Есть служба, которая следит за каталогом с довольно большим, от сотен до тысяч, количеством файлов. На основе анализа содержимого файлов строятся отчеты (до пяти отчетов разных видов). Если содержимое какого-либо файла меняется, либо меняется количество файлов, построение отчетов прерывается и начинается заново. Функциональность построения отчета реализована в классе
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 – помогла разобраться.
