Home > Software design >  Qt6 multi-threading issue
Qt6 multi-threading issue

Time:12-19

I'm reading this and this doc. But I'm still new to multi-threading and I cannot fully understand these topic.

Qt 6.2.0 under Ubuntu 20.04. Basically I have this function:

bool Flow::checkNfc()
{
    QByteArray id;
    QByteArray data;

    bool ret = _nfc.read(&id, &data, 8);
    if (ret)
    {
        // do something
    }

    return ret;
}

That tries to read an NFC tag and if it finds it, does something. This function:

bool ret = _nfc.read(&id, &data, 8);

calls in turn some libnfc functions that block the current thread. I just need to execute this function in another thread in order to avoid the "stuttering" of my application.

Because both checkNfc and _nfc.read functions need to exchange data with the main thread, I'm not sure if I can use the QFutureWatcher approach. I tried something like:

QFutureWatcher<bool> watcher;
QFuture<bool> future = QtConcurrent::run(&MyProject::checkNfc);
watcher.setFuture(bool);

but it returns a so long list of compile error that I guess it's a very wrong approach. So I want to try the QThread solution. The problem is the examples are too simple for a real case scenario:

class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork(const QString &parameter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }

signals:
    void resultReady(const QString &result);
};

Anyway I tried and in my main class I wrote:

private:
    QThread _nfcThread;
    MyNfc _nfc;

private slots:
    void nfc_readResult(bool success, QByteArray id, QByteArray data);

in constructor:

_nfc.moveToThread(&_nfcThread);
connect(&_nfcThread, &QThread::finished, &_nfc, &QObject::deleteLater);
connect(&_nfc, &MyNfc::resultRead, this, &MyProject::nfc_readResult);
_nfcThread.start();

and from a timer slot:

_nfc.doWork();

in MyNfc:

signals:
    void resultRead(bool result, QByteArray id, QByteArray data);

public slots:
    void doWork();

and:

void MyNfc::doWork()
{
    QByteArray id;
    QByteArray data;

    bool ret = read(&id, &data, 8);
    emit resultRead(ret, id, data);
}

all is still working... but my main application still blocks every time I call doWork().

What am I missing?

CodePudding user response:

You cannot call doWork() directly from the main thread because in that case it would be called in the main thread which would be blocked then. Exactly as you observe now.

So the correct way you trigger the worker to do the work in a secondary thread is this connection:

connect(&_nfcThread, &QThread::started, &_nfc, &MyNfc::doWork);

Then you only need to start your thread and when it gets started it will call doWork().

There are more errors however in your code. You should not connect to deleteLater() because your thread is a member of your main class and will be deleted when your main class gets destroyed. If you call deleteLater() before then you will get double delete which is undefined behaviour.

So your critical section of the code should be:

_nfc.moveToThread(&_nfcThread);
connect(&_nfcThread, &QThread::started, &_nfc, &MyNfc::doWork); // this starts the worker
connect(&_nfc, &MyNfc::resultRead, this, &MyProject::nfc_readResult); // this passes the result
connect(&_nfc, &MyNfc::resultRead, &_nfcThread, &QThread::quit); // this will quit the event loop in the thread
_nfcThread.start();

UPDATE: If you want to call the slot periodically, then connect to a timer. And do not quit the thread when the work is done. The simplest case would be:

_nfc.moveToThread(&_nfcThread);
connect(&_nfcThread, &QThread::started, &_timer, &QTimer::start); // this starts the timer after the thread is ready
connect(&_timer, &QTimer::timeout, &_nfc, &MyNfc::doWork); // start work at timer ticks
connect(&_nfc, &MyNfc::resultRead, this, &MyProject::nfc_readResult); // this passes the result
_nfcThread.start();

However because you did not quit the event loop, you need to quit it manually just before you delete the thread. The best place would be in the destructor of your main class.

MainClass::~MainClass()
{
  _nfcThread.quit(); // this schedules quitting of the event loop when the thread gets its current work done
  _nfcThread.wait(); // this waits for it, this is important! you cannot delete a thread while it is working on something.
  // ... because the thread (and the worker too) will get deleted immediately once this destructor body finishes, which is... right now!
}

Note however, that with this solution you must ensure that the timer ticks come slower than it takes to process the data. Otherwise you would fill up a queue of processing requests which would wait very long to be processed and will not let the thread finish until all of them get satisfied.

  • Related