No event loop or use of QTimer in non-GUI/Qt threads

Posted by me102 on Tue, 21 Sep 2021 18:35:35 +0200

[Write before]

QTimer in Qt is fairly simple and powerful.

However, you have recently encountered a number of problems using QTimer without the Qt thread.

Read the document carefully and discover details you don't notice (I'm silly T.), which require a cycle of events:

In multithreaded applications, you can use QTimer in any thread that has an event loop. 

In a multithreaded application, you can use QTimer on any thread that has an event loop.

On the other hand, QEventLoop (Qt Event Loop) is least dependent on QCoreApplication (or QGuiApplication / QApplication).

Therefore, there are two ways to use QTimer in event-free loops or non-GUI/Qt threads:

1. Since we need an event loop, we can create one by ourselves: use the local QEventLoop.

2. Use Qt threads with event loops: QThread s.

[Beginning of Text]

  • First, we have a non-Qt thread, here is C++11 thread:
std::thread thread([]{
    qDebug() << "std::thread id =" << QThread::currentThreadId();
    QTimer timer;
    timer.setInterval(1000);
    QObject::connect(&timer, &QTimer::timeout, []{
        qDebug() << "std::thread 1000 ms time out";
    });
    QEventLoop eventLoop;
    timer.start();
    eventLoop.exec();
});

If you want to use QTimer in it, you must make it have an event loop, so you need to create a QEventLoop manually.

Now that this std::thread also has an event loop, you can manually control when it ends.

However, if it is further encapsulated, is that not QThread, and why not do more.

In fact, many times, non-Qt threads do not refer to simple examples here, but rather to tripartite libraries/modules.

This is only useful when the callbacks (and so on) they provide are in the thread.

  • In a different way, we can implement a uniformly managed set of QTimer s, which then reside in a thread with an event loop called QThread:
class TimerTask
{
public:
    TimerTask()
    {
        m_thread.start();
    }

    ~TimerTask()
    {
        for (auto &timer: m_timers) {
            if (timer->isActive()) timer->stop();
            timer->deleteLater();
        }

        m_thread.quit();
        m_thread.wait();
    }

    template<typename Task>
    void addTask(int interval, bool repeat, Task task)
    {
        QTimer *timer = new QTimer;
        timer->moveToThread(&m_thread);
        timer->setInterval(interval);
        timer->setSingleShot(!repeat);
        QObject::connect(timer, &QTimer::timeout, task);

        if (!repeat)
            QObject::connect(timer, &QTimer::timeout, timer, &QTimer::deleteLater);
        else
            m_timers.append(timer);

        QMetaObject::invokeMethod(timer, "start");
    }

private:
    QThread m_thread;
    QList<QTimer *> m_timers;
};

The key addTask is to move the created QTimer to the internal QThread via QObject::moveToThread(), and then the rest is to process some minor details (at this point, the QTimer is already on another thread, so it should be OK to call it directly using QMetaObject::invokeMethod).

Next, use to see the effect (method 1 and method 2 together, should not affect 134):

Then all the code (as concise as possible):

#include <QCoreApplication>
#include <QDebug>
#include <QTimer>
#include <QThread>

#include <thread>

class TimerTask
{
public:
    TimerTask()
    {
        m_thread.start();
    }

    ~TimerTask()
    {
        for (auto &timer: m_timers) {
            if (timer->isActive()) timer->stop();
            timer->deleteLater();
        }

        m_thread.quit();
        m_thread.wait();
    }

    template<typename Task>
    void addTask(Task task, int interval, bool repeat = true)
    {
        QTimer *timer = new QTimer;
        timer->moveToThread(&m_thread);
        timer->setInterval(interval);
        timer->setSingleShot(!repeat);
        QObject::connect(timer, &QTimer::timeout, task);

        if (!repeat)
            QObject::connect(timer, &QTimer::timeout, timer, &QTimer::deleteLater);
        else
            m_timers.append(timer);

        QMetaObject::invokeMethod(timer, "start");
    }

private:
    QThread m_thread;
    QList<QTimer *> m_timers;
};

void printCurrentThreadId() {
    qDebug() << "TimerTask thread id =" << QThread::currentThreadId();
}

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

    std::thread thread([]{
        qDebug() << "std::thread id =" << QThread::currentThreadId();
        QTimer timer;
        timer.setInterval(1000);
        QObject::connect(&timer, &QTimer::timeout, []{
            qDebug() << "std::thread 1000 ms time out";
        });
        QEventLoop eventLoop;
        timer.start();
        eventLoop.exec();
    });

    TimerTask tasks;
    tasks.addTask([]{ qDebug() << "1000 ms time out"; }, 1000, true);
    tasks.addTask([]{ qDebug() << "2000 ms time out"; }, 2000, true);
    tasks.addTask(&printCurrentThreadId, 0, false);

    qDebug() << "main app thread id =" << QThread::currentThreadId();

    while (true) {
        QThread::msleep(1000);
    }
}

[Conclusion]

Every four months, in fact, it is very troublesome to write articles, and in order to ensure the correctness, there are a lot of relevant data to read, to test and so on, which is quite troublesome.

Then, I have a lot to write in my plan (because I've learned a lot) and it will write slowly, so remember to pay attention to it: -).

Finally, the code is in Github: GitHub - mengps/QtExamples: Share various Qt examples, maybe useful~https://github.com/mengps/QtExamples

Topics: Qt Multithreading