Search code examples
c++multithreadingqtqthreadqtimer

Is the timer being started from another thread?


QThread documentation suggests two ways to make code run in a separate thread. If I subclass QThread and reimplement run(), then I get

QBasicTimer::start: Timers cannot be started from another thread  

-

#include <QWidget>
#include <QThread>
#include <QBasicTimer>
#include <QDebug>
#include <QEvent>
#include <QCoreApplication>

class Worker : public QThread
{
    Q_OBJECT
    int id;
    bool m_abort = false;
    bool compute = false;
public:
    Worker() {}

protected:
    void timerEvent(QTimerEvent *event) override {
        if (event->timerId() == id) {
            compute = true;
        } else {
            QObject::timerEvent(event);
        }
    }
public slots:
    void abort() {m_abort = true;}
    void run() {
        qDebug() << QThread::currentThreadId();
        QBasicTimer timer;
        id = timer.timerId();
        timer.start(1000, this);
        forever {
            if (m_abort) break;
            QCoreApplication::processEvents();
            if (compute)
                qDebug() << "computed";
            compute = false;
        }
    }
};

class MainWidget : public QWidget
{
    Q_OBJECT
    QThread thread;
    Worker* worker;
public:
    MainWidget()
    {
        qDebug() << QThread::currentThreadId();
        worker = new Worker;
        worker->start();
    }
    ~MainWidget(){worker->abort();}
};

1) Is the timer being started from another thread?
2) Why I don't get that warning when QBasicTimer is replaced by QTimer?
3) Why I don't get that warning when using moveToThread?

#include <QWidget>
#include <QThread>
#include <QBasicTimer>
#include <QDebug>
#include <QEvent>
#include <QCoreApplication>

class Worker : public QObject
{
    Q_OBJECT
    QBasicTimer* timer;
    bool m_abort = false;
    bool compute = false;
public:
    Worker() {}

protected:
    void timerEvent(QTimerEvent *event) override {
        if (event->timerId() == timer->timerId()) {
            compute = true;
        } else {
            QObject::timerEvent(event);
        }
    }
public slots:
    void abort() {m_abort = true;}
    void run() {
        timer = new QBasicTimer;
        timer->start(1000, this);
        forever {
            if (m_abort) break;
            QCoreApplication::processEvents();
            if (compute)
                qDebug() << "computed";
            compute = false;
        }
    }
};

class MainWidget : public QWidget
{
    Q_OBJECT
    QThread thread;
    Worker* worker;
public:
    MainWidget()
    {
        worker = new Worker;
        worker->moveToThread(&thread);
        connect(this, &MainWidget::start, worker, &Worker::run);
        thread.start();
        emit start();
    }
    ~MainWidget(){worker->abort(); thread.quit(); thread.wait();}
signals:
    void start();
};        

Solution

  • Regarding the first (non-moveToThread) example...

    A quick look at the Qt source for QBasicTimer::start shows the following...

    void QBasicTimer::start(int msec, QObject *obj)
    {
        QAbstractEventDispatcher *eventDispatcher = QAbstractEventDispatcher::instance();
    
        // ...
    
        if (Q_UNLIKELY(obj && obj->thread() != eventDispatcher->thread())) {
            qWarning("QBasicTimer::start: Timers cannot be started from another thread");
            return;
        }
    

    So it expects its second argument obj to have a thread affinity equal to the current thread.

    In your Worker::run implementation, however, you have...

    timer.start(1000, this);
    

    In this context the current thread is the new thread created by the QThread instance but this refers to the QWorker instance created by the MainWidget on the main GUI thread. Hence the warning.

    Edit 1:

    To the question...

    why it works with moveToThread()?

    Consider the implementation of the MainWidget ctor...

    MainWidget()
    {
        worker = new Worker;
        worker->moveToThread(&thread);
        connect(this, &MainWidget::start, worker, &Worker::run);
        thread.start();
        emit start();
    }
    

    By the time Worker::run is called the Worker instance has been moved to the new thread. So when the line...

    timer.start(1000, this);
    

    executes, this (which refers to the Worker instance) is on the current thread and the thread affinity test in QBasicTimer::start passes without warning.

    Sorry if the above is a bit convoluted but the important thing is to consider the thread affinity of the second arg to QBasicTimer::start: it must be the currently running thread.