Циклы событий и обработка сигнального слота при использовании многопоточности в 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 в другой поток. Все его методы (без сна) будут выполняться быстро и не будут замораживать графический интерфейс.