QThread signals/slots – Why your calls stay in the main thread

Qt has great cross-platform thread support.  QThread nicely wraps up threading on many platforms and is much easier to use than raw pthreads.  Qt also supports sending synchronous or asynchronous signals across threads using their meta-object-based signal/slot mechanism.  Signals and slots are a lightweight mechanism for wiring up events between objects in UI or even non-UI code in Qt.  However, it is almost impossible not to make a simple mistake that causes the great cross-thread signal/slot mechanism to fail to be invoked (causing events to fire in the calling thread without being transmitted across threads).

The problem that you will encounter is documented, but it doesn’t stand out in the documentation and you must read between the lines to understand it.  The root of the problem is that the cross-thread signals/slots only send messages using the event loop when the sender lives in a different thread than the destination QObject that is to receive the message.  This article points out how to move the new object into the new thread.

When you construct a new QThread object, it is “owned” in Qt terms by the thread that created it (in the example below, this means it is owned by the main thread).  Thus, Qt will simply call the slot function on your QThread object using the main thread if you send a signal to your QThread from the main thread; the mechanism of passing methods between threads never gets invoked because Qt checks and sees that the owning thread of the target QObject is the same as the sending thread.

To work around the issue you simply need to update Qt’s records of which thread “owns” your QObject that you are sending a signal to.  You can update this record keeping by calling QObject::moveToThread(this) in the constructor of your QThread-derived class after calling start() on the thread.  Now Qt will see that the sender and the target QObject are owned by different threads when you emit a signal that connects to a slot on your QThread-derived object; the signal will be properly queued and passed between the event loops and invoked in the context of the background thread as you likely intended.

This issue came to light for me again while working on my Morse Code Beeper.  After carefully receiving Win32 window messages from my keyboard hook in the main thread and passing them to a background QThread using a queued signal/slot I noticed that the Properties dialog box was freezing for 0.5-1 seconds after every key stroke typed into the dialog.  This made no sense because I was passing the messages to a background-thread for the blocking Beep() function to play; the UI should have remained responsive.  I quickly remembered this issue from X-Win32 development and compared the main() thread id and the thread id of the function playing the beeps; they were the same :)

The output of the program below indicates that the bgNoMove event() slot gets invoked on the main thread (not what you want) while the bgMoved event() slot gets invoked on the background thread (what you want):

Foreground thread id: 5556
Background thread id: 5556
Background thread id: 3940

I hope this will save someone else a little time when implementing their first threaded Qt application.

#include <QtCore/QCoreApplication>
#include <QThread>

class Background : public QThread
{
    Q_OBJECT
public:
    Background(bool moveToThread)
    {
        this->start();
        if (moveToThread)
            QObject::moveToThread(this);
    }
    ~Background()
    {
        this->quit();
        this->wait();
    }

protected:
    void run()
    {
        this->exec();
    }

public slots:
    void event()
    {
        qDebug("Background thread id: %d", (int) QThread::currentThreadId());
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // Construct and start the background threads
    Background bgNoMove(false);
    Background bgMoved(true);

    qDebug("Foreground thread id: %d", (int) QThread::currentThreadId());

    // Call slot via queued mechanism
    QMetaObject::invokeMethod(&bgNoMove, "event", Qt::QueuedConnection);
    QMetaObject::invokeMethod(&bgMoved,  "event", Qt::QueuedConnection);

    return a.exec();
}

Leave a Reply