The Absent Piece on Qt Multithreading in C++

C++ programmers aim to create strong multithreaded applications with Qt, but multithreading is tricky due to race conditions, synchronization issues, deadlocks, and livelocks. Determined developers often turn to StackOverflow for solutions. However, finding the ideal solution from numerous answers can be challenging, especially considering each solution’s drawbacks.

Multithreading is a popular programming and execution model enabling multiple threads within a single process. These threads share process resources while executing independently. The model provides a useful abstraction of concurrent execution for developers. Additionally, multithreading facilitates parallel execution on multiprocessing systems using a single process.

Wikipedia

This article aims to compile essential knowledge about concurrent programming in the Qt framework, particularly focusing on commonly misunderstood areas. Readers should have prior Qt and C++ experience to grasp the content.

Selecting Between QThreadPool and QThread

Qt offers various multithreading tools, making the choice difficult initially. However, the decision-making boils down to two options: letting Qt manage threads or managing them manually. Several key factors influence this decision:

  1. Tasks that don’t require the event loop, specifically those not using signal/slot mechanisms during execution. Use: QtConcurrent and QThreadPool + QRunnable.

  2. Tasks employing signal/slots, thus needing the event loop. Use: Worker objects moved to + QThread.

Qt’s flexibility allows circumventing the “missing event loop” issue by adding one to QRunnable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MyTask : public QObject, public QRunnable
{
    Q_OBJECT
    
public:
    void MyTask::run() {
        _loop.exec();  
    }
    
public slots:
    // you need a signal connected to this slot to exit the loop,
    // otherwise the thread running the loop would remain blocked...
    void finishTask() {
        _loop.exit();
    }
    
private:
    QEventLoop _loop;
}

However, avoid such “workarounds” as they are risky and inefficient: if a thread pool thread (running MyTask) gets blocked waiting for a signal, it cannot execute other tasks from the pool.

alt text

Overriding QThread::run() allows running a QThread without an event loop. This is acceptable with proper understanding; for example, quit() won’t function as expected.

Running Single Task Instances Concurrently

Ensuring only one task instance executes at a time, with pending requests queued, is crucial when accessing exclusive resources like writing to a shared file or using TCP sockets.

Let’s temporarily set aside computer science and the producer-consumer pattern for a simple, common scenario.

A straightforward solution might involve using a QMutex. Acquiring the mutex within the task function effectively serializes threads, ensuring only one runs the function at a time. However, this impacts performance due to high contention, as threads block on the mutex before proceeding. Numerous threads actively using such a task while performing other work will spend most of their time idle.

1
2
3
4
5
void logEvent(const QString & event) {  
    static QMutex lock;  
    QMutexLocker locker(& lock);   // high contention!  
    logStream << event;            // exclusive resource  
}  

To mitigate contention, a queue and a dedicated worker thread are needed, reflecting the classic producer-consumer pattern. The worker (consumer) processes requests from the queue sequentially, while each producer adds requests to the queue. This might suggest using QQueue and QWaitCondition, but let’s explore achieving this without those primitives:

  • Utilize QThreadPool with its built-in task queue.

Or

  • Leverage the default QThread::run() with its QEventLoop.

The first approach involves setting QThreadPool::setMaxThreadCount(1) and using QtConcurrent::run() for scheduling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Logger: public QObject
{
public:
    explicit Logger(QObject *parent = nullptr) : QObject(parent) {
        threadPool.setMaxThreadCount(1);
    }

    void logEvent(const QString &event) {
        QtConcurrent::run(&threadPool, [this, event]{
            logEventCore(event);
        });
    }

private:
    void logEventCore(const QString &event) {
        logStream << event;
    }

    QThreadPool threadPool;
};

This benefits from QThreadPool::clear(), enabling instant cancellation of pending requests, useful for swift application shutdowns. However, a significant drawback is thread-affinity: logEventCore likely executes in different threads between calls. Qt has classes requiring thread-affinity, including QTimer, QTcpSocket, and potentially others.

Qt documentation states that timers started in one thread cannot be stopped from another. Only the thread owning a socket can use it. Thus, timers must be stopped in their originating thread, and QTcpSocket::close() must be called from the owning thread. These actions usually occur in destructors.

A better solution utilizes the QEventLoop provided by QThread. The concept involves using signals and slots for requests, with the thread’s event loop acting as a queue, processing one slot at a time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// the worker that will be moved to a thread
class LogWorker: public QObject
{
    Q_OBJECT

public:
    explicit LogWorker(QObject *parent = nullptr);

public slots:
    // this slot will be executed by event loop (one call at a time)
    void logEvent(const QString &event);
};

LogWorker’s constructor and logEvent implementations are straightforward and omitted here. A service is needed to manage the thread and worker instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// interface
class LogService : public QObject
{
    Q_OBJECT
    
public:
    explicit LogService(QObject *parent = nullptr);
    ~LogService();

signals:
    // to use the service, just call this signal to send a request:
    // logService->logEvent("event");
    void logEvent(const QString &event);

private:
    QThread *thread;
    LogWorker *worker;
};

// implementation
LogService::LogService(QObject *parent) : QObject(parent) {
    thread = new QThread(this);
    worker = new LogWorker;
    worker->moveToThread(thread);
    connect(this, &LogService::logEvent, worker, &LogWorker::logEvent);
    connect(thread, &QThread::finished, worker, &QObject::deleteLater);
    thread->start();
}

LogService::~LogService() {
    thread->quit();
    thread->wait();
}
alt text

Here’s how this code functions:

  • The constructor creates the thread and worker instance. The worker has no parent, as it’s moved to the new thread. Thus, Qt won’t automatically release the worker’s memory, requiring manual deletion by connecting QThread::finished to deleteLater. The proxy method LogService::logEvent() connects to LogWorker::logEvent() using Qt::QueuedConnection due to different threads.
  • The destructor queues the quit event in the event loop. This event is handled after all others. For instance, numerous logEvent() calls before destruction are processed before the logger handles the quit event. This takes time, necessitating wait() until the event loop exits. Logging requests posted after the quit event are never processed.
  • Logging (LogWorker::logEvent) always occurs in the same thread, suitable for thread-affinity-requiring classes. The LogWorker constructor and destructor execute in the main thread (where LogService runs), demanding caution with code execution. Avoid stopping timers or using sockets in the worker’s destructor unless it runs in the same thread.

Executing Worker Destructors in the Same Thread

Workers dealing with timers or sockets require destructor execution in their dedicated thread. Subclassing QThread and deleting the worker within QThread::run() achieves this. Consider this template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <typename TWorker>
class Thread : QThread
{
public:
    explicit Thread(TWorker *worker, QObject *parent = nullptr)
        : QThread(parent), _worker(worker) {
        _worker->moveToThread(this);
        start();
    }

    ~Thread() {
        quit();
        wait();
    }

    TWorker worker() const {
        return _worker;
    }

protected:
    void run() override {
        QThread::run();
        delete _worker;
    }

private:
    TWorker *_worker;
};

Redefining the previous LogService example using this template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// interface
class LogService : public Thread<LogWorker>
{
    Q_OBJECT

public:
    explicit LogService(QObject *parent = nullptr);

signals:
    void **logEvent**(const QString &event);
};

// implementation
LogService::**LogService**(QObject *parent)
    : Thread<LogWorker>(new LogWorker, parent) {
    connect(this, &LogService::logEvent, worker(), &LogWorker::logEvent);
}

Here’s how it works:

  • LogService becomes the QThread object to implement the custom run() function. Private subclassing prevents accessing QThread functions, allowing internal control over the thread’s lifecycle.
  • Thread::run() runs the event loop via the default QThread::run() implementation, destroying the worker instance after the event loop exits. The worker’s destructor executes in the same thread.
  • LogService::logEvent() acts as the proxy (signal) posting the logging event to the thread’s event queue.

Pausing and Resuming Threads

Suspending and resuming custom threads is another useful capability. Imagine pausing processing when the application minimizes, locks, or loses network connectivity. Building a custom asynchronous queue for pending requests until worker resumption is possible. However, for simplicity, we’ll utilize the event loop’s queue for this purpose.

Suspending a thread requires waiting on a condition. While blocked, the event loop doesn’t process events, and Qt queues them. Upon resumption, the event loop handles accumulated requests. We’ll use a QWaitCondition object (requiring a QMutex) for the condition. For reusability, the suspend/resume logic goes into a base class called SuspendableWorker, supporting two methods:

  • suspend(): A blocking call setting the thread to wait on a condition. This involves posting a suspend request to the queue and waiting for its handling, similar to QThread::quit() followed by wait().
  • resume(): Signals the wait condition to wake the thread for continued execution.

Interface and implementation review:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// interface
class SuspendableWorker : public QObject
{
    Q_OBJECT

public:
    explicit SuspendableWorker(QObject *parent = nullptr);
    ~SuspendableWorker();

    // resume() must be called from the outer thread.
    void resume();

    // suspend() must be called from the outer thread.
    // the function would block the caller's thread until
    // the worker thread is suspended.
    void suspend();

private slots:
    void suspendImpl();

private:
    QMutex _waitMutex;
    QWaitCondition _waitCondition;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// implementation
SuspendableWorker::SuspendableWorker(QObject *parent) : QObject(parent) {
    _waitMutex.lock();
}

SuspendableWorker::~SuspendableWorker() {
    _waitCondition.wakeAll();
    _waitMutex.unlock();
}

void SuspendableWorker::resume() {
    _waitCondition.wakeAll();
}

void SuspendableWorker::suspend() {
    QMetaObject::invokeMethod(this, &SuspendableWorker::suspendImpl);
    // acquiring mutex to block the calling thread
    _waitMutex.lock();
    _waitMutex.unlock();
}

void SuspendableWorker::suspendImpl() {
    _waitCondition.wait(&_waitMutex);
}

Suspended threads never receive quit events. Using this with standard QThread is unsafe unless resumed before posting “quit.” Let’s integrate this into our custom Thread<T> template for robustness.

alt text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
template <typename TWorker>
class Thread : QThread
{
public:
    explicit Thread(TWorker *worker, QObject *parent = nullptr)
        : QThread(parent), _worker(worker) {
        _worker->moveToThread(this);
        start();
    }

    ~Thread() {
        resume();
        quit();
        wait();
    }

    void suspend() {
        auto worker = qobject_cast<SuspendableWorker*>(_worker);
        if (worker != nullptr) {
            worker->suspend();
        }
    }

    void resume() {
        auto worker = qobject_cast<SuspendableWorker*>(_worker);
        if (worker != nullptr) {
            worker->resume();
        }
    }

    TWorker worker() const {
        return _worker;
    }

protected:
    void run() override {
        QThread::*run*();
        delete _worker;
    }

private:
    TWorker *_worker;
};

These changes ensure thread resumption before posting the quit event. Additionally, Thread<TWorker> accepts any worker type, whether a SuspendableWorker or not.

Usage example:

1
2
3
4
5
6
LogService logService;
logService.logEvent("processed event");
logService.suspend();
logService.logEvent("queued event");
logService.resume();
// "queued event" is now processed.

volatile vs atomic

This is often misunderstood. Many assume volatile variables for flags accessed by multiple threads prevent data races, which is incorrect. QAtomic* classes (or std::atomic) are necessary for this.

Consider a TcpConnection class in a dedicated thread, requiring a thread-safe method: bool isConnected(). Internally, it listens for socket events (connected and disconnected) to maintain a boolean flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    // this is not thread-safe!
    bool isConnected() const {
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        _connected = false;
    }
    
private:
    bool _connected;
}

Making _connected volatile doesn’t guarantee thread-safety for isConnected(). It might work most of the time but fail critically otherwise. Protecting variable access from multiple threads is crucial. Using QReadWriteLocker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    bool isConnected() const {
        QReadLocker locker(&_lock);
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        QWriteLocker locker(&_lock);
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        QWriteLocker locker(&_lock);
        _connected = false;
    }
    
private:
    QReadWriteLocker _lock;
    bool _connected;
}

This ensures reliability but isn’t as fast as lock-free atomic operations. The third solution offers speed and thread-safety (using std::atomic instead of QAtomicInt, but semantically equivalent):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    bool isConnected() const {
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        _connected = false;
    }
    
private:
    std::atomic<bool> _connected;
}

Conclusion

This article covered important concurrent programming concerns in Qt, designing solutions for specific use cases. Simple topics like atomic primitives, read-write locks, and others were omitted. If interested in these, leave a comment and request such a tutorial.

For those exploring Qmake, consider reading the recently published “The Vital Guide to Qmake.”

Licensed under CC BY-NC-SA 4.0