The Eight Rules of Multithreaded Qt The biggest dos and don'ts for multi-threading in Qt
While the concept of multithreading may be straightforward, code with threads is responsible for some wicked bugs, which can be nearly impossible to reproduce or track down. This makes writing bullet-proof code using threads a tall order. Let’s look a little deeper into why that is.
First, you need better than average knowledge about the internals of your frameworks, language, and compiler to know how to avoid threading trouble-spots. Second, you need to know about synchronization primitives and appropriate design patterns so you can create multi-threaded code that operates correctly under all conditions. And finally you need to understand how to use debugging tools with multiple threads to be able to find those tricky to reproduce issues that are inherent in multithreading bugs.
When it comes to Qt and multithreading, it’s especially true that you need to know your framework and design patterns. Qt gives you the power to make amazing multithreaded apps – as well as shoot your foot off. We’ve honed our multi-threading expertise over the years by finding and fixing threading bugs in both the Qt framework and Qt client code. Here’s a short list of our top rules for avoiding the most common pitfalls to have your Qt apps run right the first time:
1. Never call QThread::sleep()
Although there’s an API to allow your thread to sleep, that is QThread::sleep() – if you’re calling it you should really consider an event-driven design. By changing “threads that sleep” into “threads that wait for events” (or, better all, no threads at all), you’ll save a huge amount of system resources that are otherwise wasted by idle threads. QThread::sleep() is also bad for timing since the amount of time it takes before control is returned is poorly constrained. Sleeping threads can also theoretically cause problems during application termination; foreground threads can prevent the application from terminating until they awake, while background threads may never reawaken, preventing clean finalization.
2. Never do GUI operations off the main thread
Qt’s GUI operations are not thread safe, so non-main threads cannot safely perform any GUI operation. That means no widgets, QtQuick, QPixmap, or anything that touches the window manager. There are some exceptions: GUI functions that only manipulate data but don’t touch the window manager can be called on secondary threads, things like QImage and QPainter. Be careful though, as classes like QBitmap or QPixmap actually are not safe. Check the documentation for each API: if you don’t see a note at the top of the documentation saying that the function is reentrant, it’s not safe to be called except from the main thread.
3. Don’t block the main thread
Don’t call any function that can block for an unspecified amount of time on the main thread (like QThread::wait()). Since these functions stop the main thread from running, all event processing halts and your UI freezes. If you wait long enough, the OS application manager will think your app is frozen and ask the user if they want to kill it – not cool. Both are recipes for an unfriendly app.
4. Always destroy QObjects on the thread that owns them
Qt isn’t designed to allow you to destroy a QObject from any thread that doesn’t own it. That means that before a QThread is destroyed, all QObjects that the thread owns need to be destroyed first. Failing to clean up properly can cause data integrity issues, like the ever popular memory leaks and/or crashes.
How do you ensure the correct thread is the one destroying your QObject? Either create it as an automatic variable inside the QThread’s run() method, connect QThread::finished() to QObject::deleteLater(), or delay the destruction by moving the QObject to another thread with moveToThread(). Note that once you move a QObject away from the thread that owns it, you cannot touch it any more using that thread; you must use the new thread that owns the object.
5. Don’t trust your intuition when it comes to synchronization
A very common design pattern is that one thread signals its status to a monitoring thread, usually by writing to a boolean state variable that the monitoring thread can poll. With a data structure of a single word, only one thread writing to it, and only one thread reading it, it seems like this situation wouldn’t actually require concurrency protection since the read is guaranteed to happen eventually, right? Actually, even this simple case isn’t safe.
The C++ standard says that thread synchronization is mandatory and anything outside of the specification can result in undefined behaviour. If you’re not synchronizing – even in a “simple” case – you’re asking for trouble. In fact, some serious bugs have been found in the Linux kernel, in situations exactly as described here. The best thing to do is to not overthink what is safe or not – if there are concurrent accesses to the same data from multiple threads, no matter how unlikely they are to cause problems, protect them with appropriate synchronization mechanisms.
6. Act as if QObject is non-reentrant
A reentrant function means that as long as different threads are dealing with different data, it can be safely used without synchronization. The Qt documentation indicates that QObject is reentrant, but there are many caveats to this re-entrancy:
- Event-based classes aren’t reentrant (timers, sockets, etc.)
- Event dispatching for a given QObject happens in the thread it has affinity with; this can cause races within Qt if you touch the object from another thread
- All QObjects in the same parent/child tree must have the same thread affinity
- You must delete all QObjects owned by a thread before deleting the QThread
- You can only call moveToThread() on an object from the thread the object has affinity with
To avoid all of these special cases, it’s usually easier to just act as if QObject isn’t reentrant. In practice, this means that you should only touch a QObject on the thread that owns it. This will keep you out of all the non-obvious corner cases that can cause trouble.
7. Avoid adding slots to QThread
Because QThread objects have affinity to the original thread that created them and (perhaps unintuitively) do not have affinity to themselves, this causes issues when trying to use signals and slots on a non-main thread. Although a design where a non-main thread uses a slot can be done, since it needs to side-step a lot of non-obvious gotchas our recommendation is that you just avoid this design.
If you don’t need to override QThread:run(), then don’t subclass QThread at all; just create an instance of it and you’ll avoid problems with slots (see links at the end of this blog post for my talk for how to do this with workers).
8. Use standard library threads if it’s more natural
Finally, both the C++ standard library as well as other third party libraries have a wide array of threading classes that aren’t part of Qt – parallel algorithms, coroutines, latches, barriers, atomic smart pointers, continuations, executors, concurrent queues, distributed counters, and the like.
Qt’s multi-threading capabilities are still better in some cases: for example, Qt has thread pools while the C++ standard still does not. The good news is that the C++ classes are all compatible with Qt and can be freely incorporated into your code. In fact, unless a thread manipulates QObjects and you must use Qt threads, either C++ or Qt threading classes can be used depending on what you prefer.
If you liked this short summary, you may want to watch my full QThread talk given at QtCon or see my presentation slides on this topic. These delve much deeper into the reasons behind these rules, as well as providing code samples for the dos and don’ts.