Multithreading with Qt

QThread, worker objects, thread pools, and safe cross-thread signal/slot communication.

8 min read
60415 chars

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

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

ToolUse Case
Worker object + moveToThreadBest practice — clean, safe, full event loop
QThread::run() subclassSimple loops without slots
QtConcurrent::run()One-shot async tasks
QtConcurrent::mapped()Parallel data processing
QMutex / QMutexLockerProtect shared data
QWaitConditionProducer/consumer synchronization
Qt::QueuedConnectionSafe 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::QueuedConnection to 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::run for simple one-shot tasks that don’t need signals/slots or an event loop. Use a worker object with moveToThread when 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

ResourceLink
Qt Docs: QThreadhttps://doc.qt.io/qt-6/qthread.html
Qt Docs: Thread Supporthttps://doc.qt.io/qt-6/threads.html
Qt Docs: QtConcurrenthttps://doc.qt.io/qt-6/qtconcurrent-index.html
Qt Docs: QMutexhttps://doc.qt.io/qt-6/qmutex.html

Next tutorial → Embedded HMI Development