int countDown;
int id;
public:
LiftOff(int count, int ident = 0) :
countDown(count), id(ident) {}
~LiftOff() {
std::cout << id << " completed" << std::endl;
}
void run() {
while(countDown--)
std::cout << id << ":" << countDown << std::endl;
std::cout << "Liftoff!" << std::endl;
}
};
#endif // LIFTOFF_H ///:~
As usual, we are careful not to use any using namespace directives in a header file. The identifier id allows you to distinguish between multiple instances of the task. If you only make a single instance, you can use the default value for ident. The destructor will allow you to see that a task is properly destroyed.
In the following example, the task’s run( ) is not driven by a separate thread; it is simply called directly in main( ):
//: C11:NoThread.cpp
#include "LiftOff.h"
int main() {
LiftOff launch(10);
launch.run();
} ///:~
When a class inherits Runnable, it must have a run( ) function, but that’s nothing special—it doesn’t produce any innate threading abilities.
To achieve threading behavior, you must use the Thread class.
Using Threads
To drive a Runnable object with a thread, you create a separate Thread object and hand a Runnable pointer to the Thread’s constructor. This performs the thread initialization and then calls the Runnable’s run( ) as an interruptible thread. By driving LiftOff with a Thread, the example below shows how any task can be run in the context of another thread:
//: C11:BasicThreads.cpp
// The most basic use of the Thread class.
//{L} ZThread
#include "LiftOff.h"
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;
int main() {
try {
Thread t(new LiftOff(10));
cout << "Waiting for LiftOff" << endl;
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
Synchronization_Exception is part of the ZThread library and is the base class for all ZThread exceptions. It will be thrown if there is an error starting or using a thread.
A Thread constructor only needs a pointer to a Runnable object. It will perform the necessary initialization, call that Runnable’s run( ) member function to start the task, and then return to the calling thread, which in this case is the thread for main( ). In effect, you have made a member function call to LiftOff::run( ), and that function has not yet finished, but you can still perform other operations in the main( ) thread. (This ability is not restricted to the main( ) thread—any thread can start another thread.) You can see this by running the program. Even though LiftOff::run( ) has been called, the "Waiting for LiftOff" message will appear before the countdown has completed. Thus, the program is running two functions at once—LiftOff::run( ) and main( ).
You can easily add more threads to drive more tasks. Here, you can see how all the threads run in concert with one another:
//: C11:MoreBasicThreads.cpp
// Adding more threads.
//{L} ZThread
#include "LiftOff.h"
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;
int main() {
const int sz = 5;
try {
for(int i = 0; i < sz; i++)
Thread t(new LiftOff(10, i));
cout << "Waiting for LiftOff" << endl;
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
The second argument for the LiftOff constructor identifies each task. When you run the program, you’ll see that the execution of the different tasks is mixed together as the threads are swapped in and out. This swapping is automatically controlled by the thread scheduler. If you have multiple processors on your machine, the thread scheduler will quietly distribute the threads among the processors.
The for loop can seem a little strange at first, because t is being created locally inside the for loop and then immediately goes out of scope and is therefore destroyed. This makes it appear that the thread itself might be immediately lost, but you can see from the output that the threads are indeed running to conclusion. When you create a Thread object, the associated thread is registered with the threading system, which keeps it alive. Even though the stack-based Thread object is lost, the thread itself lives on until its associated task completes. Although this may be counterintuitive from a C++ standpoint, the concept of threads is a departure from the norm: a thread creates a separate thread of execution that persists after the function call ends. This departure is reflected in the persistence of the underlying thread after the object vanishes.
Creating responsive user interfaces
As stated earlier, one of the motivations for using threading is to create a responsive user interface. Although we don’t cover graphical user interfaces in this book, you can still see a simple example of a console-based user interface.
The following example reads lines from a file and prints them to the console, sleeping (suspending the current thread) for a second after each line is displayed. (You’ll learn more about sleeping later in the chapter). During this process, the program doesn’t look for user input, so the UI is unresponsive:
//: C11:UnresponsiveUI.cpp
// Lack of threading produces an unresponsive UI.
//{L} ZThread
#include "zthread/Thread.h"
#include <iostream>
#include <fstream>
using namespace std;
using namespace ZThread;
int main() {
const int sz = 100; // Buffer size;
char buf[sz];
cout << "Press <Enter> to quit:" << endl;
ifstream file("UnresponsiveUI.cpp");
while(file.getline(buf, sz)) {
cout << buf << endl;
Thread::sleep(1000); // Time in milliseconds
}
// Read input from the console
cin.get();
cout << "Shutting down..." << endl;
} ///:~
To make the program responsive, you can execute a task that displays the file in a separate thread. The main thread can then watch for user input so the program becomes responsive:
//: C11:ResponsiveUI.cpp