Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Qt Thread Synchronization with QWaitCondition

Notes May 15 1
  1. Common Qt Thread Synchronization Classes

QMutex, QMutexLocker, QReadWriteLocker, QReadLocker, QWriteLocker, QSemaphore, QWaitCondition

  1. QWaitCondition

2.1 Overview

QWaitCondition allows threads to signal that a specific condition has been met. One or more threads can block waiting for a QWaitCondition to be triggered using wakeOne() or wakeAll(). The wakeOne() function wakes up a randomly selected thread, while wakeAll() wakes up all waiting threads.
QWaitCondition also enables multiple threads to access the same resource simultaneously, offering higher efficiency than using a mutex alone, similar to QSemaphore.

2.2 Member Function Descriptions

  • QWaitCondition::QWaitCondition()
    Creates a condition variable object.
  • QWaitCondition::~QWaitCondition()
    Destroys the condition variable object.
  • void QWaitCondition::notify_all()
    This function is provided for STL compatibility. It is equivalent to wakeAll().
  • void QWaitCondition::notify_one()
    This function is provided for STL compatibility. Its equivalent to wakeOne().
  • bool QWaitCondition::wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
    Releases lockedMutex and waits for the condition to be triggered. lockedMutex must initially be locked by the calling thread. If lockedMutex is not locked, the behavior is undefined. If lockedMutex is a recursive mutex, the function returns immediately. lockedMutex will be unlocked, and the calling thread will block until one of the following conditions is met:
    1. Another thread signals it using wakeOne() or wakeAll(). In this case, the function returns true.
    2. The specified time in milliseconds has passed. If the time is ULONG_MAX (default), the wait never times out (the event must be signaled). If the wait times out, the function returns false.
      lockedMutex will return to the same locked state. This function ensures the transition from locked to waiting state is atomic.
  • bool QWaitCondition::wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX)
    Releases lockedReadWriteLock and waits for the condition to be triggered. lockedReadWriteLock must initially be locked by the calling thread. If lockedReadWriteLock is not locked, the function returns immediately. lockedReadWriteLock cannot be recursively locked, otherwise the function may not release the lock correctly. lockedReadWriteLock will be unlocked, and the calling thread will block until one of the following conditions is met:
    1. Another thread signals it using wakeOne() or wakeAll(). In this case, the function returns true.
    2. The specified time in milliseconds has passed. If the time is ULONG_MAX (default), the wait never times out (the event must be signaled). If the wait times out, the function returns false.
      lockedReadWriteLock will return to the same locked state. This function ensures the transition from locked to waiting state is atomic.
  • void QWaitCondition::wakeAll()
    Wakes up all threads currently waiting on the condition. The order in which threads are awakened depends on the operating system's scheduling policy and is not controllable or predictable.
  • void QWaitCondition::wakeOne()
    Wakes up one thread waiting on the condition. Which thread is awakened depends on the operating system's scheduling policy and is not controllable or predictable.
    If you want to wake a specific thread, the solution is usually to use different conditions and let different threads wait on different conditions.

2.3 Code Example

2.3.1 Global Variables

 const int DataSize = 100000;

 const int BufferSize = 8192;
 char buffer[BufferSize];

 QWaitCondition dataAvailable;
 QWaitCondition spaceAvailable;
 QMutex lock;
 int usedBytes = 0;


  • DataSize represents the amount of data the producer will generate. For simplicity, we set it as a constant.
  • BufferSize defines the size of the circular buffer. It is smaller than DataSize, meaning at some point the producer will reach the end of the buffer and restart from the beginning.
    To synchronize the producer and consumer, we need two condition variables and a mutex.
  • dataAvailable: When the producer generates data, it signals this condition to notify the consumer that data is available for reading.
  • spaceAvailable: When the consumer reads data, this condition is signaled to inform the producer that more data can be generated.
  • lock is used to ensure atomic operations on shared resources.
  • usedBytes indicates the number of bytes currently in the buffer, i.e., how many bytes are available.
    The condition variables, mutex, and usedBytes counter together ensure the producer never exceeds the consumer's capacity and the consumer never reads data that hasn't been generated yet.

2.3.2 Producer Class

 class Producer : public QThread
 {
 public:
     Producer(QObject *parent = nullptr) : QThread(parent)
     {
     }

     void run() override
     {
         for (int i = 0; i < DataSize; ++i) {
             lock.lock();
             if (usedBytes == BufferSize)
                 spaceAvailable.wait(&lock);
             lock.unlock();

             buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];

             lock.lock();
             ++usedBytes;
             dataAvailable.wakeAll();
             lock.unlock();
         }
     }
 };


The producer generates DataSize bytes of data. Before writing to the circular buffer, it checks whether the buffer is full (i.e., usedBytes equals BufferSize). If the buffer is full, the thread waits on the spaceAvailable condition.
Finally, the producer increments usedBytes and signals the dataAvailable condition, since usedBytes must be greater than zero. We protect all accesses to the usedBytes variable with a mutex. Additionally, the QWaitCondition::wait() function takes a mutex as an argument. This mutex is unlocked before the thread sleeps and relocked when the thread wakes up. Moreover, the transition from locked to waiting state is atomic, preventing race conditions.

2.3.3 Consumer Class

 class Consumer : public QThread
 {
 public:
     Consumer(QObject *parent = nullptr) : QThread(parent)
     {
     }

     void run() override
     {
         for (int i = 0; i < DataSize; ++i) {
             lock.lock();
             if (usedBytes == 0)
                 dataAvailable.wait(&lock);
             lock.unlock();

             fprintf(stderr, "%c", buffer[i % BufferSize]);

             lock.lock();
             --usedBytes;
             spaceAvailable.wakeAll();
             lock.unlock();
         }
         fprintf(stderr, "\n");
     }
 };


The code is very similar to the producer. Before reading, we check if the buffer is empty (usedBytes is 0), and if so, we wait on the dataAvailable condition. After reading, we decrement usedBytes (instead of incrementing) and signal the spaceAvailable condition (instead of dataAvailable).

2.3.4 Main Function

 int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     Producer producer;
     Consumer consumer;
     producer.start();
     consumer.start();
     producer.wait();
     consumer.wait();
     return 0;
 }


In main(), we create two threads and call QThread::wait() to ensure both threads complete before we exit.
What happens when we run the program? Initially, the producer thread is the only one that can do anything; the consumer is blocked, waiting for the dataAvailable condition to be signaled (usedBytes is not zero). Once the producer places a byte into the buffer, usedBytes becomes BufferSize - 1, and the dataAvailable condition is signaled. At this point, two things can happen: either the consumer thread takes over and reads the byte, or the producer generates the second byte.
The producer-consumer model provided in this example makes it possible to write highly concurrent multithreaded applications. On multiprocessor machines, the program could run twice as fast as an equivalent mutex-based program because the two threads can be active on different parts of the buffer simultaneously.
However, it's important to realize that these benefits are not always achievable. Locking and unlocking QMutexes comes at a cost. In practice, it is better to divide the buffer into chunks and operate on the chunks rather than individual bytes. The buffer size is also a parameter that must be carefully chosen based on the specific scenario.

Source: Qt official documentation
Code source

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.