Event loops and signal-slot processing when using multithreading in Qt

Moomin picture Moomin · Jan 20, 2015 · Viewed 16.9k times · Source

I've been having some problems with using QThreads which made me explore different combinations before I've found the right one. However I still don't fully understand what is really happening in the four cases shown below when it comes to event loops and signal-slot processing.

I added some comments to OUTPUT section, but as you can see I'm not sure if my assumptions about what caused observed behaviors are correct. Also I'm not sure if case 3 is something that might be used in real code. Here is my test code (only the main.cpp differs for each case):

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 (case 1 - no separate thread for 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 - both slot1 and slot2 called at a.exec() (??? - using main event loop?):

starting 
Worker(0x112db20) processing started 
Worker(0x112db20) processing finished 
finished 
end
slot2 
slot2 
slot2 
slot2 
slot2 
slot1 

main.cpp (case 2 - workerManager moved to separate thread, but thread not started):

#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 - neither slot1 nor slot2 was called - (??? event loop associated with thread receives signals but since thread was not started slots are not called?):

starting 
Worker(0x112db20) processing started 
Worker(0x112db20) processing finished 
finished 
end

main.cpp (case 3 - workerManager moved to separate thread, thread started but workerManager::process() called via 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 called while Worker still executing its process() (???):

starting 
Worker(0x197bb20) processing started 
slot2 
slot2 
slot2 
slot2 
Worker(0x197bb20) processing finished 
finished 
end 
slot2 
slot1 

main.cpp (case 4 - workerManager moved to separate thread, thread started but workerManager::process() called using started() signal from 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 - all events processed after reaching a.exec() (???):

end 
starting 
Worker(0x7f1d700013d0) processing started 
Worker(0x7f1d700013d0) processing finished 
finished 
slot2 
slot2 
slot2 
slot2 
slot2 
slot1 

Thanks for any clarifications.

Answer

Pavel Strakhov picture Pavel Strakhov · Jan 21, 2015

All results you got are perfectly correct. I'll try to explain how this works.

An event loop is an internal loop in Qt code that processes system and user events. Event loop of main thread is started when you call a.exec(). Event loop of another thread is started by default implementation of QThread::run.

When Qt decides it's time to process an event, it executes its event handler. While event handler is working, Qt has no chance to process any other event (unless given directly by QApplication::processEvents() or some other methods). Once the event handler is finished, control flow returns to the event loop and Qt may execute another handler to process another event.

Signals and slots are not the same as events and event handlers in Qt terminology. But slots are handled by event loops somewhat similarily. If you have control flow in your code(such as in main function) you can execute any slot immediately just as any other C++ function. But when Qt does that it can only do that from an event loop. It should be noted that signals are always sent immediately, while slot execution may be delayed.

Now let's see what happens in each case.

Case 1

WorkerManager::process is executed directly at the program start. New thread is started and Worker::process is executed immediately in the new thread. WorkerManager::process continues execution until Worker is done, freezing all other actions (including slot processing) in main thread. After WorkerManager::process is finished, control flow goes to QApplication::exec. Qt establishes connection to the other thread, receives messages about slot invokation and invokes all of them consequently.

Case 2

Qt by default executes slots of an object in the thread this object belongs to. Main thread will not execute slots of WorkerManager because it belongs to another thread. However this thread is never started. Its event loop is never finished. Invokations of slot1 and slot2 are left forever in Qt's queue waiting for you to start the thread. Sad story.

Case 3

In this case WorkerManager::process is executed in main thread because you invoke it directly from main thread. Meanwhile, WorkerManager's thread is started. Its event loop is launched and waiting for events. WorkerManager::process starts Worker's thread and executes Worker::exec in it. Worker starts sending signals to WorkerManager. WorkerManager's thread almost immediately starts to execute appropriate slots. At this point it seems awkward that WorkerManager::slot2 and WorkerManager::process are executed simultaneously. But it's perfectly fine, at least if WorkerManager is thread-safe. Shortly after Worker is done, WorkerManager::process is finished and a.exec() is executed but has not much to process.

Case 4

Main function just launches WorkerManager's thread and immediately goes to a.exec(), resulting in end as first line in the output. a.exec() processes something and ensures program execution but doesn't execute WorkerManager's slots because it belongs to another thread. WorkerManager::process is executed in WorkerManager's thread from its event loop. Worker's thread is started and Worker::process starts sending signals from Worker's thread to WorkerManager's thread. Unfortunately the latter is busy executing WorkerManager::process. When Worker is done, WorkerManager::process also finishes and WorkerManager's thread immediately executes all queued slots.

The largest problem in your code is usleep and infinite loops. You should almost never use those when working with Qt. I understand that a sleep in Worker::process is just a placeholder for some real calculation. But you should remove sleep and infinite loop from WorkerManager. Use WorkerManager::slot1 to detect Worker's termination. If you develop a GUI application there would be no need to move WorkerManager to another thread. All its methods (without sleep) will be executed fast and will not freeze the GUI.