Platform-independent threads are a rarity in C++ open-source libraries, because threads are handled in different ways by different operating systems. On the other hand, it is not possible to write GUI applications without threads, since a user click may take just an instant, while the work that needs to be done in response may take much longer. Users are accustomed to being able to continue moving and clicking while their work is being done, and seeing constant progress feedback at the same time.
A single process can simulate multitasking by supporting multiple threads. Each thread has its own call stack and a current statement to execute. A multithreaded process iterates through the threads, switching to each thread's stack, executing some statements for awhile, and then switching to the next thread. Qt's thread model permits the prioritizing and control of threads.
12.2.1. QPixmap and QThread Animation Example: Movie Player
In this section we write a multithreaded animation viewer application. This application loops through a sequence of eight images on the screen. The user controls the time interval between images to speed up or slow down the process. Figure 12.4 shows the two main classes for this application.
Figure 12.4. UML diagram for Movie and MovieView
We will start with the top-level code in Example 12.13 and then drill down to the lower layers of code.
Example 12.13. src/threads/animate/moviethreadmain.cpp
[ . . . . ] int main(int argc, char** argv) { QApplication app(argc, argv); MovieView view; MovieThread movie; app.connect(&movie, SIGNAL(show(const QPixmap*)), &view, SLOT(showPix(const QPixmap*))); app.connect(&view, SIGNAL(intervalChanged(int)), &movie, SLOT(setInterval(int))); app.connect(&app, SIGNAL(aboutToQuit()), &movie, SLOT(stop())); movie.start(); <-- 1 view.show(); return app.exec(); } [ . . . . ]
|
The interface for starting a thread is similar to that of starting a processin both cases, start() returns immediately. Instead of running another program, the newly created thread runs in the same process, shares the same memory, and starts by calling the run() function.
The MovieThread does not actually display anythingit is a model that stores the data representing the movie. Periodically, it emits a signal containing a pointer to the correct pixmap to display as seen in Example 12.14.
Example 12.14. src/threads/animate/moviethread.cpp
[ . . . . ] void MovieThread::run() { int current(0), picCount(m_Pics.size()); while (true) { msleep(m_Delay); emit show(&m_Pics[current]); current = (current + 1) % picCount; } } |
In Example 12.15 we can see the slot that actually displays the images and the slot that responds to the slider signals. These slots are in the view class, as they should be.
Example 12.15. src/threads/animate/movieview.cpp
[ . . . . ] void MovieView::showPix(const QPixmap* pic) { label->setPixmap(*pic); } void MovieView::setDelay(int newValue) { QString str; str = QString("%1ms delay between frames").arg(newValue); slider->setToolTip(str); emit intervalChanged(newValue); } |
To summarize: the run() function emits a signal from the (model) MovieThread object, which will be received by a slot in the (view) MovieView object, as arranged by the connect() statement in the main() function (shown in Example 12.13). Signals are ideal for transmitting data to objects across threads. In Example 12.16 we see how the view is created, and how the slider controls the speed of the animation.
Example 12.16. src/threads/animate/movieview.cpp
[ . . . . ] MovieView::MovieView() { resize(200, 200); slider = new QSlider(Qt::Vertical); slider->setRange(1, 500); slider->setTickInterval(10); slider->setValue(100); slider->setToolTip("How fast is it spinning?"); connect(slider, SIGNAL(valueChanged(int)), this, SLOT(setDelay(int))); QDockWidget *qdw = new QDockWidget("Delay"); qdw->setWidget(slider); addDockWidget(Qt::LeftDockWidgetArea, qdw); label = new QLabel("Movie"); setCentralWidget(label); } |
The user controls the time interval between image exposures with a QSlider widget that is placed in a dock widget.
In Example 12.17, to avoid a crash on exit, we introduced a delay that makes sure the thread has enough time to be properly terminated.
Example 12.17. src/threads/animate/moviethread.cpp
[ . . . . ] void MovieThread::stop() { terminate(); wait(5000); } |
Putting it all together, we have produced a movie of a spinning yin-yang symbol, using resources, signals, slots, and threads, with an interface that gives the user control of the spin speed via a docked slider widget. The following figure is a screenshot of the running program.
To load images from disk, we use the Qt resource feature (discussed in Section 11.4), which permits us to "embed" binary files into the executable.
Example 12.18. src/threads/animate/moviethread.cpp
[ . . . . ] void MovieThread::loadPics() { FileVisitor fv("*.jpg"); connect (&fv, SIGNAL(foundFile(const QString&)), this, SLOT(addFile(const QString&))); fv.processEntry(":/images/"); <-- 1 } void MovieThread::addFile(const QString& filename) { m_Pics << QPixmap(filename); }
|
12.2.2. Movie Player with QTimer
The MovieThread object, discussed in Section 12.2.1, simply emitted a signal periodically, with the interval determined by the user. This is a very simple use of a thread.
The QTimer class is well suited for emitting periodic signals to drive animations and other rapid but brief operations.[5] In Example 12.19 we derive our movie model class from QTimer instead of deriving it from QThread.
[5] Connecting a QTimer signal to a slot that takes a relatively long time to execute may slow down the main thread and cause the QTimer to miss clock ticks.
Example 12.19. src/threads/animate/movietimer.h
[ . . . . ] class MovieTimer :public QTimer { Q_OBJECT public: MovieTimer(); ~MovieTimer(); void loadPics(); public slots: void nextFrame(); void addFile(const QString& filename); void setInterval(int newDelay); signals: void show(const QPixmap *image); private: QVector pics; int current; }; [ . . . . ] |
The MovieTimer class can be used in place of the MovieThread class of the previous example. We need to change only one line of the client code from Example 12.13, as we show in Example 12.20.
Example 12.20. src/threads/animate/movietimermain.cpp
[ . . . . ] int main(int argc, char** argv) { QApplication app(argc, argv); MovieView view; MovieTimer movie; app.connect(&movie, SIGNAL(show(const QPixmap*)), &view, SLOT(showPix(const QPixmap*))); app.connect(&view, SIGNAL(intervalChanged(int)), &movie, SLOT(setInterval(int))); app.connect(&app, SIGNAL(aboutToQuit()), &movie, SLOT(stop())); movie.start(); <-- 1 view.show(); return app.exec(); } [ . . . . ]
|
In fact, the implementation of MovieTimer is simpler than that of MovieThread, as shown in Example 12.21.
Example 12.21. src/threads/animate/movietimer.cpp
[ . . . . ] MovieTimer::MovieTimer(): current(0) { setInterval(100); loadPics(); connect(this, SIGNAL(timeout()), this, SLOT(nextFrame())); } void MovieTimer::nextFrame() { current = (current + 1) % pics.size(); emit show(&pics[current]); } |
QTimer is very convenient and can be used in some situations where, at first, you might think of using a QThread. A QTimer with a timeout of 0 will emit its signal as fast as possible, but only when the Qt event queue is empty. By rewriting a "worker thread" to operate in small chunks when its slot is repeatedly called, you can achieve something similar to multithreading without slowing down the user interface response.
12.2.3. Multiple Threads, Queues, and Loggers Example: Giant
In this section, we present an example of a consumer-producer problem inspired by the famous fable, "Jack and the Beanstalk." As we see in Figure 12.5, the main window of the application shows a split-screen view (provided by the QSplitter) of two text areas.
Figure 12.5. Giant versus Jack main window
The slider docked on the left determines the rate at which Jack steals the items on his list from the Giant. That rate has a random component. The LineEdit docked on the bottom allows the user to give Jack things to say to the Giant. Each text area is a LogWindow that monitors a named Logger. Both classes are provided in libutils and are documented in our API Docs.[6]
[6] We discussed how to obtain and install libutils in Section 7.4.
A Logger is an abstraction for a named stream of bytes. A Logger gets attached to a LogWindow through signals and slots.
Loggers are important tools for debugging (and, sometimes, even visualizing) multitasking processes. These are based on the Java loggers, but take advantage of Qt's signals and slots, which can send messages and objects across threads. The UML diagram in Figure 12.6 shows how these classes are related.
Figure 12.6. Loggers
When this program executes, two threads are running, Jack and Giant. Each thread emits signals and writes to loggers independently. The sequence diagram in Figure 12.7 shows how this works in one thread.
Figure 12.7. Speaking with signals
Jack and Giant have no knowledge of each otherthey are strictly decoupled. They do not include each other's header files, and they are connected to each other exclusively by signals and slots in controller code from GiantWindow. These relationships are depicted in the UML diagram in Figure 12.8.
Figure 12.8. Jack and Giant UML
Each thread has an incoming message queue (fifo). Messages may arrive in the Giant's queue faster than he can handle them. Some messages will be responded to, some will be ignored.
To make things more interesting, we have connected Jack's Logger's data signal to the Giant's hear slot, so the Giant is somewhat aware of what Jack is doing and can react to it. We show the class definition for Giant in Example 12.22.
Example 12.22. src/threads/giant/giant.h
[ . . . . ] class Giant :public QThread { Q_OBJECT public: Giant(); void run(); public slots: void hear(const QString& text); void die(); void setDelay(int newDelay); signals: void say(QString line); void small(QString line); void giantDead(); private: void process(QString message); QString distort(QString text); QQueue m_Fifo; int m_Delay; bool m_IsDead; }; [ . . . . ] |
Even without threads, both Jack and Giant have slots and receive signals emitted from the Qt event loop, as we see in Example 12.23.
Example 12.23. src/threads/giant/giant.cpp
[ . . . . ] void Giant::die() { if (m_IsDead) return; m_IsDead = true; m_Fifo << "What? You nasty little worm."; m_Fifo << "I will squash you!!! "; m_Fifo << "So you're running back down the beanstalk."; m_Fifo << "I am coming right after you!"; msleep(m_Delay); <-- 1 m_Fifo << "Oh no!! Someone chopped the beanstalk!!"; m_Fifo << "aaaaaaaaa!!! ........."; m_Fifo << " *splat* "; } void Giant::hear(const QString &text) { QString t2 = ":" + text; m_Fifo << t2; }
|
In addition to responding to signals from the main thread, Jack and Giant each override the run() function. Example 12.24 shows the Giant's thread code, which is a typical "infinite" loop, reading input when there is some and sleeping when there is not.
To simulate the Giant's difficulty hearing the sounds made by a creature as tiny as Jack, we use the distort() function to introduce some noise into his process() function.
Example 12.24. src/threads/giant/giant.cpp
[ . . . . ] void Giant::run() { int zcount = 0; while (true) { zcount = 0; while (m_Fifo.isEmpty()) { msleep(m_Delay); ++zcount ; if (m_IsDead) { emit giantDead(); break; } if (zcount > 3) { m_Fifo << "zzzzzz"; } } QString message = m_Fifo.dequeue(); msleep(m_Delay); process(message); } } void Giant::process(QString message) { if(message.startsWith(":")) { QStringList l = message.split(":"); msleep(m_Delay); if(! l[1].startsWith("(")) { QString msg = l[1]; emit say ("Did I hear a mouse speak? It sounded like"); emit say (distort(msg)); emit say ("I never could understand those darned mice."); if (msg.startsWith("I FOUND")) { msg = msg.remove("I FOUND the ").remove("!!"); msg = QString("Hey! Where is my %1?").arg(msg); emit say(msg); } } } else emit say(message); } |
GiantWindow is a very primitive GUI front end for Giant. It has slots that can be hooked up to the giant's signals, as we see in Example 12.25.
Example 12.25. src/threads/giant/giantwindow.h
[ . . . . ] class GiantWindow : public QMainWindow { Q_OBJECT public: GiantWindow(); ~GiantWindow(); public slots: void speakJack(); void bigText(QString text); void smallText(QString text); void setDelay(int delayval); void terminateThreads(); private: LogWindow *m_GiantLogWindow; LogWindow *m_JackLogWindow; QLineEdit *m_LineEdit; QPushButton *m_SpeakButton, *m_KillButton; QSlider *m_GiantSlider; Giant* m_Giant; Jack* m_Jack; }; [ . . . . ] |
The constructor spells out the details of arranging things on the screen and connecting signals with slots. Example 12.26 shows some of those details.
Example 12.26. src/threads/giant/giantwindow.cpp
[ . . . . ] GiantWindow::GiantWindow() { resize(800, 600); m_Giant = new Giant(); m_Jack = new Jack(); /* The giant talks to the GiantWindow through signals and slots */ connect (m_Giant, SIGNAL(say(QString)), this, SLOT(bigText(QString))); connect (m_Giant, SIGNAL(small(QString)), this, SLOT(smallText(QString))); connect (m_Jack, SIGNAL(chopBeanstalk()), m_Giant, SLOT(die())); connect (m_Giant, SIGNAL(giantDead()), this, SLOT(terminateThreads())); m_GiantLogWindow = new LogWindow("giant"); m_GiantLogWindow->setToolTip("This is what the giant says"); m_JackLogWindow = new LogWindow("jack"); <-- 1 m_JackLogWindow->setToolTip("This is what Jack is doing"); Logger *jackLog = Logger::getLogger("jack"); connect (jackLog, SIGNAL(data(const QString&)), m_Giant, SLOT(hear(const QString&))); <-- 2 QSplitter *split = new QSplitter(this); <-- 3 split->addWidget(m_GiantLogWindow); split->addWidget(m_JackLogWindow); <-- 4 setCentralWidget(split); [ . . . . ]
|
12.2.4. Thread Safety and QObjects
A QObject that was created in a particular thread "belongs" to that thread and its children must also belong to the same thread. Having parent-child relationships that cross over threads is forbidden by Qt.
A thread-safe object is one that can be accessed concurrently by multiple threads and is guaranteed to always be in a "valid" state. QObjects are not "thread safe" by default. To make an object thread safe, there are a number of approaches to take. Some are listed here, but we recommend the Qt 4 Thread Support[7] documentation for further details.
[7] http://oop.mcs.suffolk.edu/qtdocs/threads.html
Exercise: Threads and QThread
• |
Write a program that tells the user how many prime numbers exist in a range between two numbers supplied in the command line.
|
• |
Create a QListWidget (or QListView) that shows the prime numbers found so far during the search. |
• |
Display the elapsed search time and the number of primes found per second. |
Part I: Introduction to C++ and Qt 4
C++ Introduction
Classes
Introduction to Qt
Lists
Functions
Inheritance and Polymorphism
Part II: Higher-Level Programming
Libraries
Introduction to Design Patterns
QObject
Generics and Containers
Qt GUI Widgets
Concurrency
Validation and Regular Expressions
Parsing XML
Meta Objects, Properties, and Reflective Programming
More Design Patterns
Models and Views
Qt SQL Classes
Part III: C++ Language Reference
Types and Expressions
Scope and Storage Class
Statements and Control Structures
Memory Access
Chapter Summary
Inheritance in Detail
Miscellaneous Topics
Part IV: Programming Assignments
MP3 Jukebox Assignments
Part V: Appendices
MP3 Jukebox Assignments
Bibliography
MP3 Jukebox Assignments