Chapter 14. Multithreading

Multithreading

Conventional GUI applications have one thread of execution and perform one operation at a time. If the user invokes a time-consuming operation from the user interface, the interface typically freezes while the operation is in progress. Chapter 7 presents some solutions to this problem. Multithreading is another solution.

In a multithreaded application, the GUI runs in its own thread and additional processing takes place in one or more other threads. This results in applications that have responsive GUIs even during intensive processing. When runnning on a single processor, multithreaded applications may run slower than a single-threaded equivalent due to the overhead of having multiple threads. But on multiprocessor systems, which are becoming increasingly common, multithreaded applications can execute several threads simultaneously on different processors, resulting in better overall performance.

In this chapter, we will start by showing how to subclass QThread and how to use QMutex, QSemaphore, and QWaitCondition to synchronize threads.[*] Then we will see how to communicate with the main thread from secondary threads while the event loop is running. Finally, we round off with a review of which Qt classes can be used in secondary threads and which cannot.

Multithreading is a large topic with many books devoted to the subject—for example, Threads Primer: A Guide to Multithreaded Programming by Bil Lewis and Daniel J. Berg (Prentice Hall, 1995) and Multithreaded, Parallel, and Distributed Programming by Gregory Andrews (Addison-Wesley, 2000). Here it is assumed that you already understand the fundamentals of multithreaded programming, so the focus is on explaining how to develop multithreaded Qt applications rather than on the subject of threading itself.

Creating Threads

Providing multiple threads in a Qt application is straightforward: We just subclass QThread and reimplement its run() function. To show how this works, we will start by reviewing the code for a very simple QThread subclass that repeatedly prints a given string on a console. The application’s user interface is shown in Figure 14.1.

class Thread : public QThread
{
    Q_OBJECT

public:
    Thread();

    void setMessage(const QString &message);
    void stop();

protected:
    void run();

private:
    QString messageStr;
    volatile bool stopped;
};
The Threads application

Figure 14.1. The Threads application

The Thread class is derived from QThread and reimplements the run() function. It provides two additional functions: setMessage() and stop().

The stopped variable is declared volatile because it is accessed from different threads and we want to be sure that it is freshly read every time it is needed. If we omitted the volatile keyword, the compiler might optimize access to the variable, possibly leading to incorrect results.

Thread::Thread()
{
    stopped = false;
}

We set stopped to false in the constructor.

void Thread::run()
{
    while (!stopped)
        std::cerr << qPrintable(messageStr);
    stopped = false;
    std::cerr << std::endl;
}

The run() function is called to start executing the thread. As long as the stopped variable is false, the function keeps printing the given message to the console. The thread terminates when control leaves the run() function.

void Thread::stop()
{
    stopped = true;
}

The stop() function sets the stopped variable to true, thereby telling run() to stop printing text to the console. This function can be called from any thread at any time. For the purposes of this example, we assume that assignment to a bool is an atomic operation. This is a reasonable assumption, considering that a bool can have only two states. We will see later in this section how to use QMutex to guarantee that assigning to a variable is an atomic operation.

QThread provides a terminate() function that terminates the execution of a thread while it is still running. Using terminate() is not recommended, since it can stop the thread at any point and does not give the thread any chance to clean up after itself. It is always safer to use a stopped variable and a stop() function as we did here.

We will now see how to use the Thread class in a small Qt application that uses two threads, A and B, in addition to the main thread.

class ThreadDialog : public QDialog
{
    Q_OBJECT

public:
    ThreadDialog(QWidget *parent = 0);

protected:
    void closeEvent(QCloseEvent *event);

private slots:
    void startOrStopThreadA();
    void startOrStopThreadB();

private:
    Thread threadA;
    Thread threadB;
    QPushButton *threadAButton;
    QPushButton *threadBButton;
    QPushButton *quitButton;
};

The ThreadDialog class declares two variables of type Thread and some buttons to provide a basic user interface.

ThreadDialog::ThreadDialog(QWidget *parent)
    : QDialog(parent)
{
    threadA.setMessage("A");
    threadB.setMessage("B");

    threadAButton = new QPushButton(tr("Start A"));
    threadBButton = new QPushButton(tr("Start B"));
    quitButton = new QPushButton(tr("Quit"));
    quitButton->setDefault(true);

    connect(threadAButton, SIGNAL(clicked()),
            this, SLOT(startOrStopThreadA()));
    connect(threadBButton, SIGNAL(clicked()),
            this, SLOT(startOrStopThreadB()));
    ...
}

In the constructor, we call setMessage() to make the first thread repeatedly print ‘A’s and the second thread ‘B’s.

void ThreadDialog::startOrStopThreadA()
{
    if (threadA.isRunning()) {
        threadA.stop();
        threadAButton->setText(tr("Start A"));
    } else {
        threadA.start();
        threadAButton->setText(tr("Stop A"));
    }
}

When the user clicks the button for thread A, startOrStopThreadA() stops the thread if it was running and starts it otherwise. It also updates the button’s text.

void ThreadDialog::startOrStopThreadB()
{
    if (threadB.isRunning()) {
        threadB.stop();
        threadBButton->setText(tr("Start B"));
    } else {
        threadB.start();
        threadBButton->setText(tr("Stop B"));
    }
}

The code for startOrStopThreadB() is structurally identical.

void ThreadDialog::closeEvent(QCloseEvent *event)
{
    threadA.stop();
    threadB.stop();
    threadA.wait();
    threadB.wait();
    event->accept();
}

If the user clicks Quit or closes the window, we stop any running threads and wait for them to finish (using QThread::wait()) before we call QCloseEvent::accept(). This ensures that the application exits in a clean state, although it doesn’t really matter in this example.

If you run the application and click Start A, the console will be filled with ‘A’s. If you click Start B, it will now fill with alternating sequences of ‘A’s and ‘B’s. Click Stop A, and now it will print only ‘B’s.

Synchronizing Threads

A common requirement for multithreaded applications is that of synchronizing several threads. Qt provides the following synchronization classes: QMutex, QReadWriteLock, QSemaphore, and QWaitCondition.

The QMutex class provides a means of protecting a variable or a piece of code so that only one thread can access it at a time. The class provides a lock() function that locks the mutex. If the mutex is unlocked, the current thread seizes it immediately and locks it; otherwise, the current thread is blocked until the thread that holds the mutex unlocks it. Either way, when the call to lock() returns, the current thread holds the mutex until it calls unlock(). The QMutex class also provides a tryLock() function that returns immediately if the mutex is already locked.

For example, let’s suppose that we wanted to protect the stopped variable of the Thread class from the preceding section with a QMutex. We would then add the following member variable to Thread:

private:
    ...
    QMutex mutex;
};

The run() function would change to this:

void Thread::run()
{
    forever {
        mutex.lock();
        if (stopped) {
            stopped = false;
            mutex.unlock();
            break;
        }
        mutex.unlock();

        std::cerr << qPrintable(messageStr);
    }
    std::cerr << std::endl;
}

The stop() function would become this:

void Thread::stop()
{
    mutex.lock();
    stopped = true;
    mutex.unlock();
}

Locking and unlocking a mutex in complex functions, or in functions that throw C++ exceptions, can be error-prone. Qt offers the QMutexLocker convenience class to simplify mutex handling. QMutexLocker’s constructor accepts a QMutex as argument and locks it. QMutexLocker’s destructor unlocks the mutex. For example, we could rewrite the earlier run() and stop() functions as follows:

void Thread::run()
{
    forever {
        {
            QMutexLocker locker(&mutex);
            if (stopped) {
                stopped = false;
                break;
            }
        }

        std::cerr << qPrintable(messageStr);
    }
    std::cerr << std::endl;
}

void Thread::stop()
{
    QMutexLocker locker(&mutex);
    stopped = true;
}

One issue with using mutexes is that only one thread can access the same variable at a time. In programs with lots of threads trying to read the same variable simultaneously (without modifying it), the mutex can be a serious performance bottleneck. In these cases, we can use QReadWriteLock, a synchronization class that allows simultaneous read-only access without compromising performance.

In the Thread class, it would make no sense to replace QMutex with QReadWriteLock to protect the stopped variable, because at most one thread might try to read the variable at any given time. A more appropriate example would involve one or many reader threads accessing some shared data and one or many writer threads modifying the data. For example:

MyData data;
QReadWriteLock lock;

void ReaderThread::run()
{
    ...
    lock.lockForRead();
    access_data_without_modifying_it(&data);
    lock.unlock();
    ...
}

void WriterThread::run()
{
    ...
    lock.lockForWrite();
    modify_data(&data);
    lock.unlock();
    ...
}

For convenience, we can use the QReadLocker and QWriteLocker classes to lock and unlock a QReadWriteLock.

QSemaphore is another generalization of mutexes, but unlike read-write locks, semaphores can be used to guard a certain number of identical resources. The following two code snippets show the correspondence between QSemaphore and QMutex:

QSemaphore semaphore(1);
semaphore.acquire();
semaphore.release();
  
QMutex mutex;
mutex.lock();
mutex.unlock();

By passing 1 to the constructor, we tell the semaphore that it controls a single resource. The advantage of using a semaphore is that we can pass numbers other than 1 to the constructor and then call acquire() multiple times to acquire many resources.

A typical application of semaphores is when transferring a certain amount of data (DataSize) between two threads using a shared circular buffer of a certain size (BufferSize):

const int DataSize = 100000;
const int BufferSize = 4096;
char buffer[BufferSize];

The producer thread writes data to the buffer until it reaches the end and then restarts from the beginning, overwriting existing data. The consumer thread reads the data as it is generated. Figure 14.2 illustrates this, assuming a tiny 16-byte buffer.

The producer–consumer model

Figure 14.2. The producer–consumer model

The need for synchronization in the producer–consumer example is twofold: If the producer generates the data too fast, it will overwrite data that the consumer hasn’t yet read; if the consumer reads the data too fast, it will pass the producer and read garbage.

A crude way to solve this problem is to have the producer fill the buffer, then wait until the consumer has read the entire buffer, and so on. However, on multiprocessor machines, this isn’t as fast as letting the producer and consumer threads operate on different parts of the buffer at the same time.

One way to efficiently solve the problem involves two semaphores:

QSemaphore freeSpace(BufferSize);
QSemaphore usedSpace(0);

The freeSpace semaphore governs the part of the buffer that the producer can fill with data. The usedSpace semaphore governs the area that the consumer can read. These two areas are complementary. The freeSpace semaphore is initialized with BufferSize (4096), meaning that it has that many resources that can be acquired. When the application starts, the reader thread will start acquiring “free” bytes and convert them into “used” bytes. The usedSpace semaphore is initialized with 0 to ensure that the consumer won’t read garbage at startup.

For this example, each byte counts as one resource. In a real-world application, we would probably operate on larger units (e.g., 64 or 256 bytes at a time) to reduce the overhead associated with using semaphores.

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        freeSpace.acquire();
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        usedSpace.release();
    }
}

In the producer, every iteration starts by acquiring one “free” byte. If the buffer is full of data that the consumer hasn’t read yet, the call to acquire() will block until the consumer has started to consume the data. Once we have acquired the byte, we fill it with some random data (‘A’, ‘C’, ‘G’, or ‘T’) and release the byte as “used”, so that the consumer thread can read it.

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        usedSpace.acquire();
        std::cerr << buffer[i % BufferSize];
        freeSpace.release();
    }
    std::cerr << std::endl;
}

In the consumer, we start by acquiring one “used” byte. If the buffer contains no data to read, the call to acquire() will block until the producer has produced some. Once we have acquired the byte, we print it and release the byte as “free”, making it possible for the producer to fill it with data again.

int main()
{
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

Finally, in main(), we start the producer and consumer threads. Then the producer converts some “free” space into “used” space, and the consumer can convert it back to “free” space.

When we run the program, it writes a random sequence of 100000 ‘A’s, ‘C’s, ‘G’s, and ‘T’s to the console and terminates. To really understand what is going on, we can disable writing the output and instead write ‘P’ each time the producer generates a byte and ‘c’ each time the consumer reads a byte. And to make things as simple to follow as possible, we can use smaller values for DataSize and BufferSize.

For example, here’s a possible run with a DataSize of 10 and a BufferSize of 4: “PcPcPcPcPcPcPcPcPcPc”.In this case, the consumer reads the bytes as soon as the producer generates them; the two threads are executing at the same speed. Another possibility is that the producer fills the whole buffer before the consumer even starts reading it: “PPPPccccPPPPccccPPcc”. There are many other possibilities. Semaphores give a lot of latitude to the system-specific thread scheduler, which can study the threads’ behavior and choose an appropriate scheduling policy.

A different approach to the problem of synchronizing a producer and a consumer is to use QWaitCondition and QMutex. A QWaitCondition allows a thread to wake up other threads when some condition has been met. This allows for more precise control than is possible with mutexes alone. To show how it works, we will redo the producer–consumer example using wait conditions.

const int DataSize = 100000;
const int BufferSize = 4096;
char buffer[BufferSize];

QWaitCondition bufferIsNotFull;
QWaitCondition bufferIsNotEmpty;
QMutex mutex;
int usedSpace = 0;

In addition to the buffer, we declare two QWaitConditions, one QMutex, and one variable that stores how many bytes in the buffer are “used” bytes.

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == BufferSize)
            bufferIsNotFull.wait(&mutex);
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        ++usedSpace;
        bufferIsNotEmpty.wakeAll();
        mutex.unlock();
    }
}

In the producer, we start by checking whether the buffer is full. If it is, we wait on the “buffer is not full” condition. When that condition is met, we write one byte to the buffer, increment usedSpace, and wake any thread waiting for the “buffer is not empty” condition to turn true.

We use a mutex to protect all accesses to the usedSpace variable. The QWaitCondition::wait() function can take a locked mutex as its first argument, which it unlocks before blocking the current thread and then locks before returning.

For this example, we could have replaced the while loop

while (usedSpace == BufferSize)
    bufferIsNotFull.wait(&mutex);

with this if statement:

if (usedSpace == BufferSize) {
    mutex.unlock();
    bufferIsNotFull.wait();
    mutex.lock();
}

However, this would break as soon as we allow more than one producer thread, since another producer could seize the mutex immediately after the wait() call and make the “buffer is not full” condition false again.

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == 0)
            bufferIsNotEmpty.wait(&mutex);
        std::cerr << buffer[i % BufferSize];
        --usedSpace;
        bufferIsNotFull.wakeAll();
        mutex.unlock();
    }
    std::cerr << std::endl;
}

The consumer does the exact opposite of the producer: It waits for the “buffer is not empty” condition and wakes up any thread waiting for the “buffer is not full” condition.

In all the examples so far, our threads have accessed the same global variables. But some multithreaded applications need to have a global variable hold different values in different threads. This is often called thread-local storage or thread-specific data. We can fake it using a map keyed on thread IDs (returned by QThread::currentThread()), but a nicer approach is to use the QThreadStorage<T> class.

A common use of QThreadStorage<T> is for caches. By having a separate cache in different threads, we avoid the overhead of locking, unlocking, and possibly waiting for a mutex. For example:

QThreadStorage<QHash<int, double> *> cache;

void insertIntoCache(int id, double value)
{
    if (!cache.hasLocalData())
        cache.setLocalData(new QHash<int, double>);
    cache.localData()->insert(id, value);
}

void removeFromCache(int id)
{
    if (cache.hasLocalData())
        cache.localData()->remove(id);
}

The cache variable holds one pointer to a QHash<int, double> per thread. (Because of problems with some compilers, the template type in QThreadStorage<T> must be a pointer type.) The first time we use the cache in a particular thread, hasLocalData() returns false and we create the QHash<int, double> object.

In addition to caching, QThreadStorage<T> can be used for global error-state variables (similar to errno) to ensure that modifications in one thread don’t affect other threads.

Communicating with the Main Thread

When a Qt application starts, only one thread is running—the main thread. This is the only thread that is allowed to create the QApplication or QCoreApplication object and call exec() on it. After the call to exec(), this thread is either waiting for an event or processing an event.

The main thread can start new threads by creating objects of a QThread subclass, as we did in the previous section. If these new threads need to communicate among themselves, they can use shared variables together with mutexes, read-write locks, semaphores, or wait conditions. But none of these techniques can be used to communicate with the main thread, since they would lock the event loop and freeze the user interface.

The solution for communicating from a secondary thread to the main thread is to use signal–slot connections across threads. Normally, the signals and slots mechanism operates synchronously, meaning that the slots connected to a signal are invoked immediately when the signal is emitted, using a direct function call.

However, when we connect objects that “live” in different threads, the mechanism becomes asynchronous. (This behavior can be changed through an optional fifth parameter to QObject::connect().) Behind the scenes, these connections are implemented by posting an event. The slot is then called by the event loop of the thread in which the receiver object exists. By default, a QObject exists in the thread in which it was created; this can be changed at any time by calling QObject::moveToThread().

To illustrate how signal–slot connections across threads work, we will review the code of the Image Pro application, a basic image processing application that allows the user to rotate, resize, and change the color depth of an image. The application (shown in Figure 14.3), uses one secondary thread to perform operations on images without locking the event loop. This makes a significant difference when processing very large images. The secondary thread has a list of tasks, or “transactions”, to accomplish and sends events to the main window to report progress.

ImageWindow::ImageWindow()
{
    imageLabel = new QLabel;
    imageLabel->setBackgroundRole(QPalette::Dark);
    imageLabel->setAutoFillBackground(true);
    imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
    setCentralWidget(imageLabel);

    createActions();
    createMenus();

    statusBar()->showMessage(tr("Ready"), 2000);

    connect(&thread, SIGNAL(transactionStarted(const QString &)),
            statusBar(), SLOT(showMessage(const QString &)));
    connect(&thread, SIGNAL(allTransactionsDone()),
            this, SLOT(allTransactionsDone()));

    setCurrentFile("");
}
The Image Pro application

Figure 14.3. The Image Pro application

The interesting part of the ImageWindow constructor is the two signal–slot connections. Both of them involve signals emitted by the TransactionThread object, which we will cover in a moment.

void ImageWindow::flipHorizontally()
{
    addTransaction(new FlipTransaction(Qt::Horizontal));
}

The flipHorizontally() slot creates a “flip” transaction and registers it using the private function addTransaction(). The flipVertically(), resizeImage(), convertTo32Bit(), convertTo8Bit(), and convertTo1Bit() functions are similar.

void ImageWindow::addTransaction(Transaction *transact)
{
    thread.addTransaction(transact);
    openAction->setEnabled(false);
    saveAction->setEnabled(false);
    saveAsAction->setEnabled(false);
}

The addTransaction() function adds a transaction to the secondary thread’s transaction queue and disables the Open, Save, and Save As actions while transactions are being processed.

void ImageWindow::allTransactionsDone()
{
    openAction->setEnabled(true);
    saveAction->setEnabled(true);
    saveAsAction->setEnabled(true);
    imageLabel->setPixmap(QPixmap::fromImage(thread.image()));
    setWindowModified(true);
    statusBar()->showMessage(tr("Ready"), 2000);
}

The allTransactionsDone() slot is called when the TransactionThread’s transaction queue becomes empty.

Now, let’s turn to the TransactionThread class. Like most QThread subclasses, it is somewhat tricky to implement, because the run() function executes in its own thread, whereas the other functions (including the constructor and the destructor) are called from the main thread. The class definition follows:

class TransactionThread : public QThread
{
    Q_OBJECT

public:
    TransactionThread();
    ~TransactionThread();

    void addTransaction(Transaction *transact);
    void setImage(const QImage &image);
    QImage image();

signals:
    void transactionStarted(const QString &message);
    void allTransactionsDone();

protected:
    void run();

private:
    QImage currentImage;
    QQueue<Transaction *> transactions;
    QWaitCondition transactionAdded;
    QMutex mutex;
};

The TransactionThread class maintains a queue of transactions to process and executes them one after the other in the background. In the private section, we declare four member variables:

  • currentImage holds the image onto which the transactions are applied.

  • transactions is the queue of pending transactions.

  • transactionAdded is a wait condition that is used to wake up the thread when a new transaction has been added to the queue.

  • mutex is used to protect the currentImage and transactions member variables against concurrent access.

Here is the class’s constructor:

TransactionThread::TransactionThread()
{
    start();
}

In the constructor, we simply call QThread::start() to launch the thread that will execute the transactions.

TransactionThread::~TransactionThread()
{
    {
        QMutexLocker locker(&mutex);
        while (!transactions.isEmpty())
            delete transactions.dequeue();
        transactions.enqueue(EndTransaction);
        transactionAdded.wakeOne();
    }

    wait();
}

In the destructor, we empty the transaction queue and add a special EndTransaction marker to the queue. Then we wake up the thread and wait for it to finish using QThread::wait(), before the base class’s destructor is implicitly invoked. Failing to call wait() would most probably result in a crash when the thread tries to access the class’s member variables.

The QMutexLocker’s destructor unlocks the mutex at the end of the inner block, just before the wait() call. It is important to unlock the mutex before calling wait(); otherwise, the program could end up in a deadlock situation, where the secondary thread waits forever for the mutex to be unlocked, and the main thread holds the mutex and waits for the secondary thread to finish before proceeding.

void TransactionThread::addTransaction(Transaction *transact)
{
    QMutexLocker locker(&mutex);
    transactions.enqueue(transact);
    transactionAdded.wakeOne();
}

The addTransaction() function adds a transaction to the transaction queue and wakes up the transaction thread if it isn’t already running. All accesses to the transactions member variable are protected by a mutex, because the main thread might modify them through addTransaction() at the same time as the secondary thread is iterating over transactions.

void TransactionThread::setImage(const QImage &image)
{
    QMutexLocker locker(&mutex);
    currentImage = image;
}
QImage TransactionThread::image()
{
    QMutexLocker locker(&mutex);
    return currentImage;
}

The setImage() and image() functions allow the main thread to set the image on which the transactions should be performed, and to retrieve the resulting image once all the transactions are done.

void TransactionThread::run()
{
    Transaction *transact = 0;
    QImage oldImage;

    forever {
        {
            QMutexLocker locker(&mutex);

            if (transactions.isEmpty())
                transactionAdded.wait(&mutex);
            transact = transactions.dequeue();
            if (transact == EndTransaction)
                break;

            oldImage = currentImage;
        }

        emit transactionStarted(transact->message());
        QImage newImage = transact->apply(oldImage);
        delete transact;

        {
            QMutexLocker locker(&mutex);
            currentImage = newImage;
            if (transactions.isEmpty())
                emit allTransactionsDone();
        }
    }
}

The run() function goes through the transaction queue and executes each transaction in turn by calling apply() on them, until it reaches the EndTransaction marker. If the transaction queue is empty, the thread waits on the “transaction added” condition.

Just before we execute a transaction, we emit the transactionStarted() signal with a message to display in the application’s status bar. When all the transactions have finished processing, we emit the allTransactionsDone() signal.

class Transaction
{
public:
    virtual ~Transaction() { }

    virtual QImage apply(const QImage &image) = 0;
    virtual QString message() = 0;
};

The Transaction class is an abstract base class for operations that the user can perform on an image. The virtual destructor is necessary because we need to delete instances of Transaction subclasses through a Transaction pointer. Transaction has three concrete subclasses: FlipTransaction, ResizeTransaction, and ConvertDepthTransaction. We will only review FlipTransaction; the other two classes are similar.

class FlipTransaction : public Transaction
{
public:
    FlipTransaction(Qt::Orientation orientation);

    QImage apply(const QImage &image);
    QString message();

private:
    Qt::Orientation orientation;
};

The FlipTransaction constructor takes one parameter that specifies the orientation of the flip (horizontal or vertical).

QImage FlipTransaction::apply(const QImage &image)
{
    return image.mirrored(orientation == Qt::Horizontal,
                          orientation == Qt::Vertical);
}

The apply() function calls QImage::mirrored() on the QImage it receives as a parameter and returns the resulting QImage.

QString FlipTransaction::message()
{
    if (orientation == Qt::Horizontal) {
        return QObject::tr("Flipping image horizontally...");
    } else {
        return QObject::tr("Flipping image vertically...");
    }
}

The message() function returns the message to display in the status bar while the operation is in progress. This function is called in TransactionThread::run() when emitting the transactionStarted() signal.

The Image Pro application shows how Qt’s signals and slots mechanism makes it easy to communicate with the main thread from a secondary thread. Implementing the secondary thread is trickier, because we must protect our member variables using a mutex, and we must put the thread to sleep and wake it up appropriately using a wait condition. The two-part Qt Quarterly article series “Monitors and Wait Conditions in Qt”, available online at http://doc.trolltech.com/qq/qq21-monitors.html and http://doc.trolltech.com/qq/qq22-monitors2.html, presents some more ideas on how to develop and test QThread subclasses that use mutexes and wait conditions for synchronization.

Using Qt’s Classes in Secondary Threads

A function is said to be thread-safe when it can safely be called from different threads simultaneously. If two thread-safe functions are called concurrently from different threads on the same shared data, the result is always defined. By extension, a class is said to be thread-safe when all of its functions can be called from different threads simultaneously without interfering with each other, even when operating on the same object.

Qt’s thread-safe classes include QMutex, QMutexLocker, QReadWriteLock, QReadLocker, QWriteLocker, QSemaphore, QThreadStorage<T>, and QWaitCondition. In addition, parts of the QThread API and several other functions are thread-safe, notably QObject::connect(), QObject::disconnect(), QCoreApplication::postEvent(), and QCoreApplication::removePostedEvents().

Most of Qt’s non-GUI classes meet a less stringent requirement: They are reentrant. A class is reentrant if different instances of the class can be used simultaneously in different threads. However, accessing the same reentrant object in multiple threads simultaneously is not safe, and such accesses should be protected with a mutex. Reentrant classes are marked as such in the Qt reference documentation. Typically, any C++ class that doesn’t reference global or otherwise shared data is reentrant.

QObject is reentrant, but there are three constraints to keep in mind:

  • Child QObjects must be created in their parent’s thread.

    In particular, this means that the objects created in a secondary thread must never be created with the QThread object as their parent, because that object was created in another thread (either the main thread or a different secondary thread).

  • We must delete all QObjects created in a secondary thread before deleting the corresponding QThread object.

    This can be done by creating the objects on the stack in QThread::run().

  • QObjects must be deleted in the thread that created them.

    If we need to delete a QObject that exists in a different thread, we must call the thread-safe QObject::deleteLater() function instead, which posts a “deferred delete” event.

Non-GUI QObject subclasses such as QTimer, QProcess, and the network classes are reentrant. We can use them in any thread, as long as the thread has an event loop. For secondary threads, the event loop is started by calling QThread::exec() or by convenience functions such as QProcess::waitForFinished() and QAbstractSocket::waitForDisconnected().

Because of limitations inherited from the low-level libraries on which Qt’s GUI support is built, QWidget and its subclasses are not reentrant. One consequence of this is that we cannot directly call functions on a widget from a secondary thread. If we want to, say, change the text of a QLabel from a secondary thread, we can emit a signal connected to QLabel::setText() or call QMetaObject::invokeMethod() from that thread. For example:

void MyThread::run()
{
    ...
    QMetaObject::invokeMethod(label, SLOT(setText(const QString &)),
                              Q_ARG(QString, "Hello"));
    ...
}

Many of Qt’s non-GUI classes, including QImage, QString, and the container classes, use implicit sharing as an optimization technique. Although this optimization usually makes a class non-reentrant, in Qt this is not an issue because Qt uses atomic assembly language instructions to implement thread-safe reference counting, making Qt’s implicitly shared classes reentrant.

Qt’s QtSql module can also be used in multithreaded applications, but it has its own restrictions, which vary from database to database. For details, see http://doc.trolltech.com/4.3/sql-driver.html. For a complete list of multithreading caveats, see http://doc.trolltech.com/4.3/threads.html.



[*] Qt 4.4 is expected to provide a higher-level threading API, supplementing the threading classes described here, to make writing multithreaded applications less error-prone.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset