Пользователь
0,0
рейтинг
27 августа 2012 в 12:33

Разработка → Правильное использование QThread из песочницы

Qt*
В недавнем проекте с 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
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (7)

  • +3
    А у вас действительно RBWorker обрабатывает stop до того как закончит построение отчёта?
    Вы же внутри сигнала запускаете обработку, тем самым вы останавливаете EventLoop и следующий сигнал будет доставлен уже после окончания обработки отчёта.
    Я бы предположил что построение одного отчёта — задача достаточно быстрая (в то время как построение 5000 — нет), а значит можно не отменять построение отчёта а давать ему закончить.
    В таком случае удобнее использовать QtConcurent (filter или map по вкусу) — он сам справится и с запуском потоков и с агрегацией выходных данных. Отменить обработку тоже можно, да и удобный индикатор выполнения — тоже большой плюс.
    • +2
      Стоит еще добавить, что не стоит создавать кучу потоков — какой смысл их генерить 5 тысяч штук, если ядра всего 2-4? Да и жрут они довольно много при создании/смерти, гораздо лучше использовать QThreadPool, в котором хранится оптимальное количество потоков, а задачи поступают по очереди.

      К слову, QtConcurrent это делает автоматически
  • +4
    Я просто оставлю эту ссылку здесь: Threads, Events and QObjects
    И насколько я помню, на хабре есть русский перевод, по-моему переводил пользователь surik.
    • НЛО прилетело и опубликовало эту надпись здесь
  • +2
    Если следовать данному подходу, почему бы тогда не воспользоваться пулом потоков?
    qt-project.org/doc/qt-4.8/qthreadpool.html
    либо если пойти в абстракции еще дальше то qt-project.org/doc/qt-4.8/qtconcurrentrun.html#run
  • +1
    Забавно, но не рекламы ради, посмотрите на дату моего поста. Не стал сюда писать из-за того, что особо описывать то и нечего :-).
  • +1
    Не хочу особо критиковать, но пост не то чтобы тянет на серьезный технологический. Пост, предоставленный Vass немного выше гораздо лучше. Его перевод тут уже есть, поэтому я немного сомневаюсь о целесообразности именно этого поста. А вообще — спасибо.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.