Why Threading in Qt?
Qt’s UI runs on the main thread (the event loop). Blocking it with long operations (I/O, computation, serial reads) freezes the UI. Move heavy work to worker threads and communicate back via signals/slots.
Main Thread (UI) Worker Thread
────────────────────── ──────────────────────
Event loop readSensor() runs here
Render widgets emits resultReady()
Handle user input ──────────────────────
↓ (queued connection)
UI slot updates label
Pattern 1 — Worker Object (Recommended)
Create a QObject worker and move it to a QThread. This is the safest and most idiomatic Qt approach.
// worker.h
#include <QObject>
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork(); // runs in worker thread
signals:
void resultReady(float value);
void finished();
};
// worker.cpp
#include "worker.h"
#include <QThread>
void Worker::doWork() {
for (int i = 0; i < 100; ++i) {
QThread::msleep(50); // simulate I/O
float value = i * 0.5f;
emit resultReady(value);
}
emit finished();
}
// mainwindow.cpp
#include "worker.h"
#include <QThread>
QThread *thread = new QThread(this);
Worker *worker = new Worker;
worker->moveToThread(thread);
// Wire up: thread start → worker start
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(worker, &Worker::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
// Safe cross-thread result delivery (auto queued)
connect(worker, &Worker::resultReady, this, &MainWindow::onResult);
thread->start();
void MainWindow::onResult(float value) {
// Called in main thread — safe to update UI
m_label->setText(QString::number(value, 'f', 2));
}
Pattern 2 — Subclass QThread (Simple Cases Only)
Only override run() for straightforward loops. Avoid putting slots in a QThread subclass.
class SensorThread : public QThread {
Q_OBJECT
protected:
void run() override {
while (!isInterruptionRequested()) {
float temp = readHardware();
emit temperatureRead(temp);
msleep(500);
}
}
signals:
void temperatureRead(float temp);
private:
float readHardware() { return 25.0f + (rand() % 100) / 100.0f; }
};
SensorThread *sensorThread = new SensorThread(this);
connect(sensorThread, &SensorThread::temperatureRead,
this, &MainWindow::updateTemperature);
sensorThread->start();
// Stop gracefully
sensorThread->requestInterruption();
sensorThread->wait();
Thread Pool — QtConcurrent
For parallelizing independent tasks without managing threads manually:
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
// Run a function in the global thread pool
QFuture<float> future = QtConcurrent::run([]() -> float {
// Heavy computation
float result = 0;
for (int i = 0; i < 1000000; ++i) result += i * 0.001f;
return result;
});
// Watch for completion — notifies in main thread
QFutureWatcher<float> *watcher = new QFutureWatcher<float>(this);
connect(watcher, &QFutureWatcher<float>::finished, [watcher, this]() {
m_label->setText(QString::number(watcher->result(), 'f', 2));
watcher->deleteLater();
});
watcher->setFuture(future);
Map/Reduce pattern:
QList<int> data = {1, 2, 3, 4, 5, 6, 7, 8};
// Apply function to each element in parallel
QFuture<int> mapped = QtConcurrent::mapped(data, [](int x) {
return x * x;
});
mapped.waitForFinished();
Thread Safety Rules
Safe
- Accessing Qt objects only from the thread that owns them
- Emitting signals across threads (Qt queues automatically)
- Using
QMutex,QReadWriteLock,QAtomicInt
Unsafe
- Calling UI widget methods from a non-main thread
- Accessing shared data without a lock
- Moving a QObject that has a parent
QMutex — Protecting Shared Data
#include <QMutex>
#include <QMutexLocker>
class SharedBuffer : public QObject {
Q_OBJECT
public:
void write(float value) {
QMutexLocker locker(&m_mutex); // auto-unlocks on scope exit
m_data.append(value);
}
QVector<float> read() {
QMutexLocker locker(&m_mutex);
return m_data;
}
private:
QMutex m_mutex;
QVector<float> m_data;
};
QWaitCondition — Producer/Consumer
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
class DataPipe {
public:
void produce(const QByteArray &data) {
QMutexLocker lock(&m_mutex);
m_queue.enqueue(data);
m_cond.wakeOne();
}
QByteArray consume() {
QMutexLocker lock(&m_mutex);
while (m_queue.isEmpty())
m_cond.wait(&m_mutex); // releases lock while waiting
return m_queue.dequeue();
}
private:
QMutex m_mutex;
QWaitCondition m_cond;
QQueue<QByteArray> m_queue;
};
Summary
| Tool | Use Case |
|---|---|
Worker object + moveToThread | Best practice — clean, safe, full event loop |
QThread::run() subclass | Simple loops without slots |
QtConcurrent::run() | One-shot async tasks |
QtConcurrent::mapped() | Parallel data processing |
QMutex / QMutexLocker | Protect shared data |
QWaitCondition | Producer/consumer synchronization |
Qt::QueuedConnection | Safe cross-thread signal delivery |
Advanced Topics
QReadWriteLock — Multiple Readers, One Writer
#include <QReadWriteLock>
class SensorCache : public QObject {
Q_OBJECT
public:
// Many threads can read simultaneously
float readTemp() const {
QReadLocker lock(&m_lock);
return m_temp;
}
// Only one thread writes, all readers blocked
void updateTemp(float t) {
QWriteLocker lock(&m_lock);
m_temp = t;
}
private:
mutable QReadWriteLock m_lock;
float m_temp = 0.0f;
};
QSemaphore — Counting Locks
#include <QSemaphore>
// Allow at most 3 simultaneous connections
QSemaphore connectionSlots(3);
// Acquire before connecting
if (connectionSlots.tryAcquire(1, 2000)) { // wait up to 2s
// connect...
connectionSlots.release(1); // release when done
} else {
qDebug() << "All connection slots busy";
}
Thread Pool — QThreadPool / QRunnable
#include <QThreadPool>
#include <QRunnable>
class ScanTask : public QRunnable {
QString m_target;
public:
explicit ScanTask(const QString &t) : m_target(t) {
setAutoDelete(true); // pool deletes task after run
}
void run() override {
qDebug() << "Scanning" << m_target
<< "on" << QThread::currentThread();
}
};
// Submit tasks — pool manages threads automatically
for (const auto &host : hosts) {
QThreadPool::globalInstance()->start(new ScanTask(host));
}
QThreadPool::globalInstance()->waitForDone();
Thread Affinity Visualization
┌───────────────────────────────────────────────────┐
│ Qt Process │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Main Thread │ │ Worker Thread │ │
│ │ ┌────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ Event Loop │ │ │ │ Worker::doWork │ │ │
│ │ │ UI Render │ │ │ │ (heavy I/O / │ │ │
│ │ │ User Input │ │ │ │ computation) │ │ │
│ │ └────────────┘ │ │ └────────────────┘ │ │
│ │ ↑ QueuedConnection ──────────────┘ │ │
│ └──────────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────────────┘
Lab 1 — Worker Thread with Progress
// downloader.h
class Downloader : public QObject {
Q_OBJECT
public slots:
void download(const QString &url) {
emit started();
for (int i = 0; i <= 100; i += 10) {
QThread::msleep(200); // simulate work
emit progress(i);
}
emit finished("Download complete: " + url);
}
signals:
void started();
void progress(int percent);
void finished(const QString &result);
};
// main.cpp
QThread *thread = new QThread;
Downloader *dl = new Downloader;
dl->moveToThread(thread);
QProgressBar *bar = new QProgressBar;
QLabel *stat = new QLabel("Idle");
QObject::connect(thread, &QThread::started,
dl, &Downloader::download,
Qt::QueuedConnection);
QObject::connect(dl, &Downloader::progress, bar, &QProgressBar::setValue);
QObject::connect(dl, &Downloader::finished, stat, &QLabel::setText);
QObject::connect(dl, &Downloader::finished, thread, &QThread::quit);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(thread, &QThread::finished, dl, &Downloader::deleteLater);
// Trigger by emitting started — but we need to pass the URL
// Better: use a lambda to call with arg
QObject::connect(thread, &QThread::started, [dl](){
QMetaObject::invokeMethod(dl, "download",
Q_ARG(QString, "https://example.com/firmware.bin"));
});
thread->start();
Lab 2 — Producer/Consumer with QSemaphore
const int BUFFER_SIZE = 5;
QSemaphore freeSlots(BUFFER_SIZE);
QSemaphore usedSlots(0);
QQueue<int> buffer;
QMutex bufLock;
// Producer thread
class Producer : public QThread {
void run() override {
for (int i = 0; i < 20; ++i) {
freeSlots.acquire();
QMutexLocker lk(&bufLock);
buffer.enqueue(i);
qDebug() << "Produced:" << i;
usedSlots.release();
}
}
};
// Consumer thread
class Consumer : public QThread {
void run() override {
for (int i = 0; i < 20; ++i) {
usedSlots.acquire();
QMutexLocker lk(&bufLock);
int v = buffer.dequeue();
qDebug() << "Consumed:" << v;
freeSlots.release();
}
}
};
Producer p; Consumer c;
p.start(); c.start();
p.wait(); c.wait();
Interview Questions
Q1: Why should you never call UI methods from a worker thread?
Qt’s widgets are not thread-safe. Calling UI methods from a non-main thread causes undefined behavior (crashes, rendering glitches). Always use signals/slots with
Qt::QueuedConnectionto post UI updates back to the main thread.
Q2: What is thread affinity in Qt?
Every QObject has a “thread affinity” — the thread it lives on. Slots are invoked in the object’s affinity thread. Use
moveToThread()to change a QObject’s affinity before it is used.
Q3: When should you use QtConcurrent::run vs a worker object?
Use
QtConcurrent::runfor simple one-shot tasks that don’t need signals/slots or an event loop. Use a worker object withmoveToThreadwhen you need long-running work with progress reporting, cancellation, or inter-thread communication.
Q4: What is a race condition and how do you prevent it?
A race condition occurs when two threads access shared data concurrently without synchronization, causing unpredictable results. Prevent with
QMutex,QReadWriteLock,QAtomicInt, or by designing for single-writer patterns.
Q5: Can you moveToThread a QObject that has a parent?
No. You must remove the parent before calling
moveToThread. Objects with parents cannot be moved to a different thread — Qt enforces this at runtime.
Application: Real-Time Sensor Logger
// sensor_reader.h
class SensorReader : public QObject {
Q_OBJECT
bool m_running = false;
public slots:
void start() {
m_running = true;
while (m_running) {
float temp = 25.0f + (rand() % 200 - 100) / 10.0f;
float volt = 3.3f + (rand() % 10) / 100.0f;
emit reading(QDateTime::currentMSecsSinceEpoch(), temp, volt);
QThread::msleep(500);
}
emit stopped();
}
void stop() { m_running = false; }
signals:
void reading(qint64 ts, float temp, float volt);
void stopped();
};
// logger_window.h
class LoggerWindow : public QWidget {
Q_OBJECT
public:
LoggerWindow(QWidget *p = nullptr) : QWidget(p) {
auto *lay = new QVBoxLayout(this);
m_log = new QTextEdit; m_log->setReadOnly(true);
auto *startBtn = new QPushButton("Start");
auto *stopBtn = new QPushButton("Stop");
auto *hbox = new QHBoxLayout;
hbox->addWidget(startBtn); hbox->addWidget(stopBtn);
lay->addWidget(m_log);
lay->addLayout(hbox);
// Setup thread
m_thread = new QThread(this);
m_reader = new SensorReader;
m_reader->moveToThread(m_thread);
connect(startBtn, &QPushButton::clicked, m_reader, &SensorReader::start);
connect(stopBtn, &QPushButton::clicked, m_reader, &SensorReader::stop);
connect(m_reader, &SensorReader::reading, this, &LoggerWindow::logReading);
connect(m_thread, &QThread::finished, m_reader, &QObject::deleteLater);
m_thread->start();
}
~LoggerWindow() {
m_reader->stop();
m_thread->quit();
m_thread->wait();
}
private slots:
void logReading(qint64 ts, float temp, float volt) {
m_log->append(QString("[%1] T=%2°C V=%3V")
.arg(QDateTime::fromMSecsSinceEpoch(ts).toString("hh:mm:ss"))
.arg(temp, 0, 'f', 1)
.arg(volt, 0, 'f', 2));
}
private:
QTextEdit *m_log;
QThread *m_thread;
SensorReader *m_reader;
};
References
| Resource | Link |
|---|---|
| Qt Docs: QThread | https://doc.qt.io/qt-6/qthread.html |
| Qt Docs: Thread Support | https://doc.qt.io/qt-6/threads.html |
| Qt Docs: QtConcurrent | https://doc.qt.io/qt-6/qtconcurrent-index.html |
| Qt Docs: QMutex | https://doc.qt.io/qt-6/qmutex.html |
Next tutorial → Embedded HMI Development