Qt Threading Showdown: QThreadPool/QRunnable vs moveToThread
When to use QThreadPool/QRunnable for fire-and-forget tasks versus moving a QObject to a dedicated QThread for stateful background work, with a concrete image-loader example.
13 Jul 2025, 14:11 UTC

The problem: keeping the UI responsive
In any Qt application with a graphical interface, the main thread handles user input, layout, and widget painting. If a long-running task blocks that thread, the window becomes unresponsive and the operating system may report “Not responding.” The solution is to offload work to another thread, but Qt imposes a strict rule: all QWidget manipulation must happen on the main thread. That means any background thread can compute or read data, but it must communicate results back to the GUI thread before anything gets displayed.
The thesis: two patterns for the job
Qt developers typically choose between QThreadPool + QRunnable for short, fire-and-forget tasks, and a QObject moved to a QThread for long-running or stateful work. The right choice depends on whether you need automatic thread reuse and minimal boilerplate, or full control over timers, internal state, and child objects.
1. QThreadPool + QRunnable for short tasks
The global thread pool, accessible via QThreadPool::globalInstance(), is sized at startup from the ideal CPU thread count. You create a QRunnable subclass, implement run(), and call QThreadPool::globalInstance()->start(runnable). Qt reuses threads from the pool, so you don’t manage creation or destruction. However, QRunnable is not a QObject; it has no signals of its own. To report results, you typically pair it with a small QObject “bridge” that owns the signal, and invoke the bridge’s slot from run() using QMetaObject::invokeMethod with Qt::QueuedConnection so the signal delivers on the receiver’s thread—the GUI thread, in practice.
2. QObject moved to QThread for stateful work
When your background work has internal state, needs timers, or must own child objects, the moveToThread pattern is more natural. You create a QObject subclass, give it a worker method, call worker->moveToThread(thread), connect thread->started to the worker’s init slot, and thread->start(). All slots invoked from within that thread execute in the thread’s event loop, so timers, queued connections, and child-object lifetime work as expected. The downside is more boilerplate: you must manually start the thread, watch for destruction ordering, and ensure you never call worker methods from the GUI thread directly.
3. Worked example: image loader as QRunnable
Suppose you want to load an image from disk and display it in a QLabel without freezing the UI. The run() method of the QRunnable performs the file I/O and decode, then creates a bridge object:
class ImageBridge : public QObject { Q_OBJECT public: QImage image; void setImage(const QImage &img) { image = img; emit finished(img); } signals: void finished(const QImage &); };The GUI creates the task, connects the finished signal to a lambda or slot that calls label->setPixmap(QPixmap::fromImage(img)), and adds the task to the global pool. Because the signal uses a queued connection, the slot runs on the main thread, the QLabel updates, and the UI stays interactive throughout the disk and decode work.
To verify the pattern works as expected, run the slot body and check QThread::currentThread(); it should report the main thread. A thread sanitizer or Q_ASSERT on the thread ID in the slot will catch accidental direct calls.
4. Trade‑offs and limitations
- Never touch widgets from a worker thread. Even reading a widget’s property from a non-main thread is unsafe; marshal data through signals or queued invocations instead.
- QRunnable has no signals. Results must travel through a bridge QObject or via QMetaObject::invokeMethod with a queued connection.
- QThreadPool threads have no event loop by default. Timers and queued slot delivery into a QRunnable will not work there.
- Destroying a QThread while it is running, or letting a QRunnable outlive objects it references, causes crashes. Always call QThread::wait() before destruction and be careful with ownership.
Choosing your pattern
If your task is short, stateless, and purely computational or I/O-bound, start with QThreadPool and QRunnable. The automatic thread reuse keeps overhead low, and the bridge-object pattern gives you safe signal-based results delivery. If you need timers, internal state that persists across invocations, or fine-grained control over thread lifecycle, move a QObject into a dedicated QThread instead. Either way, compile with a thread sanitizer or assert the thread ID in your receiver slots to catch affinity leaks early, and consult the Qt documentation for your exact version (5.x vs 6.x) before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.