Правильное использование QThread

в 8:33, , рубрики: Qt Software, QThread, метки:

В недавнем проекте с 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 неправильно, то как тогда правильно? После небольшого поиска, нашел вот этот пост, в котором все разложено по полочкам. Ключевые моменты поста:

  1. QThread – это не поток, а Qt обертка для потока конкретной ОС, которая позволяет взаимодействовать с потоком из Qt проекта, в первую очередь через Qt signals/slots.
  2. Выделение памяти оператором new экземплярам класса, предназначенным для выполнения в отдельном потоке должно осуществляться уже в потоке. Собственником объекта будет тот поток, который выделил объекту память.
  3. Для управления потоками и «живущими» в них объектами важно правильно настроить обмен сообщениями.

Как правильно

Итак «правильный» рецепт запуска и остановки классов в потоках:

Создаем обертку для класса, который будет жить в отдельном потоке. В нашем случае это 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js