Циклы событий и обработка сигнального слота при использовании многопоточности в Qt
У меня возникли проблемы с использованием QThreads
, из-за чего я исследовал разные комбинации, прежде чем нашел правильный. Однако я до сих пор не совсем понимаю, что действительно происходит в четырех случаях, показанных ниже, когда речь идет о циклах событий и обработке сигналов.
Я добавил несколько комментариев в раздел OUTPUT, но, как вы можете видеть, я не уверен, что мои предположения о том, что вызвало наблюдаемое поведение, верны. Также я не уверен, что case 3
- это то, что может использоваться в реальном коде. Вот мой тестовый код (только для каждого случая main.cpp
):
worker.h:
#include <QObject>
#include <QDebug>
#include <QThread>
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = 0) { this->isRunning_ = false;}
bool isRunning() const { return isRunning_; }
signals:
void processingFinished();
void inProgress();
public slots:
void process()
{
this->isRunning_ = true;
qDebug() << this << "processing started";
for (int i = 0; i < 5; i++)
{
QThread::usleep(1000);
emit this->inProgress();
}
qDebug() << this << "processing finished";
this->isRunning_ = false;
emit this->processingFinished();
}
private:
bool isRunning_;
};
workermanager.h:
#include "worker.h"
class WorkerManager : public QObject
{
Q_OBJECT
public:
explicit WorkerManager(QObject *parent = 0) :
QObject(parent) {}
public slots:
void process()
{
QThread *thread = new QThread();
Worker *worker = new Worker();
connect(thread,SIGNAL(started()),worker,SLOT(process()));
connect(worker,SIGNAL(processingFinished()),this,SLOT(slot1()));
connect(worker,SIGNAL(inProgress()),this,SLOT(slot2()));
worker->moveToThread(thread);
qDebug() << "starting";
thread->start();
QThread::usleep(500);
while(worker->isRunning()) { }
qDebug() << "finished";
}
void slot1() { qDebug() << "slot1"; }
void slot2() { qDebug() << "slot2"; }
};
main.cpp(случай 1 - отдельный поток для workerManager
):
#include <QCoreApplication>
#include "workermanager.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
WorkerManager* workerManager = new WorkerManager;
workerManager->process();
qDebug() << "end";
return a.exec();
}
OUTPUT - как slot1
, так и slot2
, вызываемые в a.exec()
(??? - с использованием основного цикла событий?):
starting
Worker(0x112db20) processing started
Worker(0x112db20) processing finished
finished
end
slot2
slot2
slot2
slot2
slot2
slot1
main.cpp(case 2 - workerManager
перемещен в отдельный поток, но поток не запущен):
#include <QCoreApplication>
#include "workermanager.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
WorkerManager* workerManager = new WorkerManager;
QThread *thread = new QThread();
workerManager->moveToThread(thread);
workerManager->process();
qDebug() << "end";
return a.exec();
}
OUTPUT - не было вызвано ни slot1
, ни slot2
- (цикл событий, связанный с потоком, принимает сигналы, но поскольку поток не был запущен, слоты не вызываются?):
starting
Worker(0x112db20) processing started
Worker(0x112db20) processing finished
finished
end
main.cpp(case 3 - workerManager
перемещен в отдельный поток, поток запущен, но workerManager::process()
вызван через workerManager->process()
):
#include <QCoreApplication>
#include "workermanager.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
WorkerManager* workerManager = new WorkerManager;
QThread *thread = new QThread();
workerManager->moveToThread(thread);
thread->start();
workerManager->process();
qDebug() << "end";
return a.exec();
}
OUTPUT - slot2
, пока Worker
выполняет process()
(???):
starting
Worker(0x197bb20) processing started
slot2
slot2
slot2
slot2
Worker(0x197bb20) processing finished
finished
end
slot2
slot1
main.cpp(case 4 - workerManager
перемещен в отдельный поток, поток запущен, но workerManager::process()
вызван с использованием started()
сигнала из thread
):
#include <QCoreApplication>
#include "workermanager.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
WorkerManager* workerManager = new WorkerManager;
QThread *thread = new QThread();
workerManager->moveToThread(thread);
QObject::connect(thread,SIGNAL(started()),workerManager,SLOT(process()));
thread->start();
qDebug() << "end";
return a.exec();
}
OUTPUT - все события обрабатываются после достижения a.exec()
(???):
end
starting
Worker(0x7f1d700013d0) processing started
Worker(0x7f1d700013d0) processing finished
finished
slot2
slot2
slot2
slot2
slot2
slot1
Спасибо за любые разъяснения.
Ответы
Ответ 1
Все полученные вами результаты совершенно правильны. Я попытаюсь объяснить, как это работает.
Цикл событий - это внутренний цикл в Qt-коде, который обрабатывает системные и пользовательские события. Контур события основного потока запускается, когда вы вызываете a.exec()
. Цикл событий другого потока запускается по умолчанию с помощью QThread::run
.
Когда Qt решает время обработки события, он выполняет обработчик событий. В то время как обработчик событий работает, Qt не имеет возможности обрабатывать какое-либо другое событие (если не указано непосредственно QApplication::processEvents()
или некоторые другие методы). Когда обработчик события закончен, поток управления возвращается в цикл событий, а Qt может выполнять другой обработчик для обработки другого события.
Сигналы и слоты не совпадают с событиями и обработчиками событий в терминологии Qt. Но слоты обрабатываются циклами событий несколько аналогично. Если у вас есть управляющий поток в вашем коде (например, в функции main
), вы можете выполнить любой слот сразу так же, как и любую другую функцию С++. Но когда Qt делает это, он может делать это только из цикла событий. Следует отметить, что сигналы всегда отправляются немедленно, в то время как выполнение слота может быть отложено.
Теперь посмотрим, что происходит в каждом случае.
Случай 1
WorkerManager::process
выполняется непосредственно при запуске программы. Запускается новый поток, и Worker::process
выполняется немедленно в новом потоке. WorkerManager::process
продолжает выполнение до тех пор, пока не будет выполнен рабочий процесс, замораживая все остальные действия (включая обработку слотов) в основном потоке. По завершении WorkerManager::process
поток управления переходит в QApplication::exec
. Qt устанавливает соединение с другим потоком, получает сообщения о вызове слота и вызывает их все.
Случай 2
Qt по умолчанию выполняет слоты объекта в потоке, к которому принадлежит этот объект. Основной поток не будет выполнять слоты WorkerManager
, потому что он принадлежит другому потоку. Однако этот поток никогда не запускается. Его цикл событий никогда не завершается. Invokations из slot1
и slot2
остаются навсегда в очереди Qt, ожидая, когда вы начнете поток. Грустная история.
Случай 3
В этом случае WorkerManager::process
выполняется в основном потоке, потому что вы вызываете его непосредственно из основного потока. Тем временем начинается поток WorkerManager
. Его цикл событий запускается и ждет событий. WorkerManager::process
запускает Worker
поток и выполняет Worker::exec
в нем. Worker
начинает посылать сигналы на WorkerManager
. WorkerManager
поток почти сразу начинает выполнять соответствующие слоты. На данный момент кажется неудобным, что WorkerManager::slot2
и WorkerManager::process
выполняются одновременно. Но это отлично, по крайней мере, если WorkerManager
является потокобезопасным. Вскоре после завершения Worker
завершается WorkerManager::process
и выполняется a.exec()
, но не так много для обработки.
Случай 4
Основная функция запускает поток WorkerManager
и сразу переходит к a.exec()
, в результате end
- первая строка на выходе. a.exec()
обрабатывает что-то и обеспечивает выполнение программы, но не выполняет WorkerManager
слоты, потому что принадлежит другому потоку. WorkerManager::process
выполняется в потоке WorkerManager
из цикла событий. Worker
запускается поток, а Worker::process
начинает посылать сигналы из потока Worker
в поток WorkerManager
. К сожалению, последний занят выполнением WorkerManager::process
. Когда Worker
выполняется, WorkerManager::process
также заканчивается, и WorkerManager
поток немедленно выполняет все очереди в очереди.
Самая большая проблема в вашем коде - usleep
и бесконечные циклы. Вы почти никогда не будете использовать их при работе с Qt. Я понимаю, что сон в Worker::process
является просто заполнитель для некоторого реального вычисления. Но вы должны удалить сон и бесконечный цикл из WorkerManager
. Используйте WorkerManager::slot1
для обнаружения завершения Worker
. Если вы разработаете приложение GUI, нет необходимости перемещать WorkerManager
в другой поток. Все его методы (без сна) будут выполняться быстро и не будут замораживать графический интерфейс.