by Jasmin Blanchette |
Qt provides a portable API for creating and synchronizing threads since version 2.2, and offers the option of building the Qt library with or without thread support. With the emergence of multi-processor computers, multithreaded programming is rapidly gaining popularity. A recent customer survey tells us that over 50% of our licensees use threads in their applications.
Qt 3's support for multithreaded programming consists of the classes QThread, QThreadStorage, QMutex, QMutexLocker, QSemaphore, and QWaitCondition. In addition, many other Qt classes are reentrant, meaning that distinct instances of these classes can be used simultaneously in different threads.
Qt 4 extends Qt 3's multithreading support with the following improvements:
Atomic Reference-Counting |
The first step in making Qt 4 more thread-friendly was to make Qt's implicitly shared classes really implicitly shared. With Qt 3, we must be careful when copying certain types of object (e.g., QString) across threads. From the Qt 3.3 documentation: Qt provides many implicitly shared and explicitly shared classes. In a multithreaded program, multiple instances of a shared class can reference shared data, which is dangerous if one or more threads attempt to modify the data. Qt provides the QDeepCopy class, which ensures that shared classes reference unique data. Internally, what happens when you take a copy is that two objects end up pointing to the same data, and the reference count associated to the data is incremented. This situation is illustrated by the diagram below:
If the objects are then modified simultaneously from different threads, the reference count may get out of sync. This problem occurs because C++'s increment and decrement operators are not guaranteed to be atomic---indeed, they typically expand to three machine instructions:
This technology is available in our public API in the form of the QSharedData and QSharedDataPointer classes. These classes are perfect if you want to write your own implicitly shared classes and want them to be copiable across threads, just like Qt's built-in tool classes.
The Atomic Operations |
The atomic operations are implemented in platform-specific header files located in Qt's "src/corelib/arch" directory. For example, the Intel 80386 implementation is located in "src/corelib/" "arch/i386/arch/qatomic.h". These operations are used internally by Qt and are not part of the public API. They may change in future versions of Qt. Qt 4 currently uses these six atomic operations:
int q_atomic_increment(volatile int *x) { register int oldValue; do { oldValue = *x; } while (!q_atomic_test_and_set_int(x, oldValue, oldValue + 1)); return oldValue != -1; }
In the loop, we atomically test that nobody altered *x behind our back and set the value to to oldValue + 1. If somebody did alter *x between the assignment to oldValue and the "test and set" operation, "test and set" fails and we try again.
Faster Mutexes and Semaphores |
The Qt 3 implementation of QMutex uses the different platform's APIs for creating, locking, and unlocking mutexes. With this approach, the very common case where lock() is called when the mutex is unlocked is relatively expensive.
Atomic operations open the door to a more efficient implementation of mutexes and semaphores (since QSemaphore is built on top of QMutex). The trick involves an internal owner pointer, which points to the thread that currently owns the mutex. The pointer is null if the mutex is unlocked. An actual system mutex is used only when the lock is already held by another thread.
To implement this algorithm, the atomic "test and set" operation is used when accessing the owner pointer. Atomic reference counting is also used to wake up threads waiting for the mutex to unlock. The result is a QMutex class that is almost as fast as atomic reference counting.
Read-Write Lock |
In Qt Quarterly 11, Volker Hilsheimer wrote an article about how to implement a read/write lock. The class was also released as a Qt Solution shortly after. Due to popular demand, we've now added QReadWriteLock to Qt, as well as two convenience locker classes (QReadLocker and QWriteLocker). From the Qt 4.0 documentation: A read-write lock is a synchronization tool for protecting resources that can be accessed for reading and writing. This type of lock is useful if you want to allow multiple threads to have simultaneous read-only access, but as soon as one thread wants to write to the resource, all other threads must be blocked until the writing is complete.
In many cases, QReadWriteLock is a direct competitor to QMutex. QReadWriteLock is a good choice if there are many concurrent reads and writing occurs infrequently. Like QMutex, the new QReadWriteLock uses atomic operations to optimize the common case where the lock isn't held for writing.
Per-Thread Event Loops |
In most Qt applications, the event loop is started by calling QApplication::exec() from main(). Qt 4 also makes it possible to start thread-specific event loops by calling QThread::exec() from a QThread::run() reimplementation.
Event loops make it possible to use non-GUI classes such as QTimer, QTcpSocket, and QProcess, which require the presence of an event loop. They also make it possible to establish signal--slot connections across threads, as we will see in the next section.
When a QObject is created, it is associated with the thread that is currently running. The object is then said to "live" in that thread; all events sent to the object from any thread will be delivered by the object's thread. Objects can be moved to another thread after their creation using QObject::moveToThread().
Signal--Slot Connections Across Threads |
In Qt 3, the usual way to communicate with the GUI thread from a non-GUI thread was by posting a custom event to a QObject in the GUI thread. In Qt 4, this still works and can be generalized to the case where one thread needs to communicate with any other thread that has an event loop.
To ease programming, Qt 4 also allows you to establish signal--slot connections across threads. Behind the scenes, these connections are implemented using an event. If the signal has any parameters, these are also stored in the event. Like previously, if the sender and receiver live in the same thread, Qt makes a direct function call.
New Reentrant Classes |
Some more Qt classes have been made reentrant in Qt 4:
Documentation & Examples |
Before you start using any of the features described in this article, we recommend that you first read the "Thread Support in Qt 4" page from the Qt reference documentation, available at "http://doc.trolltech.com/4.0/threads.php". It explains in details how to write multithreaded applications using Qt 4 and warns about common pitfalls.
Qt's "examples/threads" directory contains small programs that demonstrate Qt's thread classes. Particularly interesting is the Mandelbrot example, which shows how to use a worker thread to perform heavy computations without blocking the main thread's event loop, using signal--slot connections across threads.
If you're interested in network programming, you might also want to take a look at the Blocking Fortune Client and Threaded Fortune Server examples, which illustrate how to use QTcpSocket in a non-GUI thread (with a thread-specific event loop) and how to write multithreaded servers. These examples are located in Qt's "examples/network" directory.
Full documentation for a selection of the Qt 4.0 examples is available at "http://doc.trolltech.com/4.0/examples.php".
Copyright © 2005 Trolltech | Trademarks |