QT

Runnning lambda functions on a specific thread with Qt

Recently I’ve worked a lot with multithreading and wanted to share a very simple but useful tool. However, Qt-experts won’t be surprised.

TL;DR: The final solution can be found below.

Calling functions from another thread with signals

When I started to work with multithreading in Qt, I did all thread communication with signals and slots, because that was the only simple way I knew. I called functions from another thread using additional signals.

class Manager : public QObject
{
    Q_OBJECT
public:
    void action(const QString &parameter);
    Q_SIGNAL void actionRequested(const QString &parameter);
};

If Manager runs on thread A you can’t just call action() of course when you’re operating on thread B. However, you can emit manager->actionRequested(parameter) and Qt will call the connected slot (action()) on thread A using a Qt::QueuedConnection.

This approach has two big issues though:

  1. You need to create weird signals for every function you want to call from another thread.
  2. You can’t handle the results.

Now there are of course solutions to point 2, you can i.e. create another requested-signal on the calling object and call back, but your code will get much harder to understand.

It gets even worse if there are multiple places from where a function needs to be called. How do you know which callback to execute then?

I also knew about the old-variant of QMetaObject::invokeMethod which essentially has the same result handling problem, but additionally isn’t even checked at compile-time.

QMetaObject::invokeMethod(manager, "action", Qt::QueuedConnection,
                          Q_ARG(QString, parameter));

Executing a lambda on a QThread

I always wanted a solution that would allow to execute a lambda with captured variables as that would solve all of my issues. At some point I had the idea to create one signal/slot pair with a std::function<void()> parameter to do that, but it’s even easier. Since Qt 5.10 there’s a new version of QMetaObject::invokeMethod() that can do exactly what I needed.

auto parameter = QStringLiteral("Hello");
QMetaObject::invokeMethod(otherThreadsObject, [=] {
    qDebug() << "Hello from otherThreadsObject's thread" << parameter;
});

Note: You can’t use a QThread object as argument here, because the QThread object itself lives in the thread where it has been created.

I personally don’t like the name QMetaObject::invokeMethod, so I added an alias to my projects:

template<typename Function>
auto runOnThread(QObject *targetObject, Function function)
{
    QMetaObject::invokeMethod(targetObject, std::move(function));
}

The result handling as a caller also works pretty well:

runOnThread(object, [this, object] {
    // on object's thread
    auto value = object->action();

    runOnThread(this, [value]() {
        // on caller's thread again
        qDebug() << "Calculated result:" << value;
    });
});

One could only complain that all your code needs to be indented deeper each time you call runOnThread(). However, that could potentially be solved using C++20 coroutines.

Comments