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 ¶meter);
Q_SIGNAL void actionRequested(const QString ¶meter);
};
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:
- You need to create weird signals for every function you want to call from another thread.
- 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.