// Threading for a responsive user interface.
//{L} ZThread
#include "zthread/Thread.h"
#include <iostream>
#include <fstream>
using namespace ZThread;
using namespace std;
class DisplayTask : public Runnable {
ifstream in;
static const int sz = 100;
char buf[sz];
bool quitFlag;
public:
DisplayTask(const string& file) : quitFlag(false) {
in.open(file.c_str());
}
~DisplayTask() { in.close(); }
void run() {
while(in.getline(buf, sz) && !quitFlag) {
cout << buf << endl;
Thread::sleep(1000);
}
}
void stop() { quitFlag = true; }
};
int main() {
try {
cout << "Press <Enter> to quit:" << endl;
DisplayTask* dt = new DisplayTask("ResponsiveUI.cpp");
Thread t(dt);
cin.get();
dt->stop();
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
cout << "Shutting down..." << endl;
} ///:~
Now the main thread can respond immediately when you press <Return> and call stop( ) on the DisplayTask.
This example also shows the need for communication between tasks—the task in the main thread needs to tell the DisplayTask to shut down. Of course, since we have a pointer to the DisplayTask, you might think of just calling delete on that pointer to kill the task, but this produces unreliable programs. The problem is that the task could be in the middle of something important when you destroy it, and so you are likely to put the program in an unstable state. Here, the task itself decides when it’s safe to shut down. The easiest way to do this is by simply notifying the task that you’d like it to stop by setting a Boolean flag. When the task gets to a stable point it can check that flag and do whatever is necessary to clean up before returning from run( ). When the task returns from run( ), the Thread knows that the task has completed.
Although this program is simple enough that it should not have any problems, there are some small flaws regarding inter-task communication. This is an important topic that will be covered later in this chapter.
Simplifying with Executors
You can simplify your coding overhead by using ZThread Executors. Executors provide a layer of indirection between a client and the execution of a task; instead of a client executing a task directly, an intermediate object executes the task.
We can show this by using an Executor instead of explicitly creating Thread objects in MoreBasicThreads.cpp. A LiftOff object knows how to run a specific task; like the Command Pattern, it exposes a single function to be executed. An Executor object knows how build the appropriate context to execute Runnable objects. In the following example, the ThreadedExecutor creates one thread per task:
//: c11:ThreadedExecutor.cpp
//{L} ZThread
#include "zthread/ThreadedExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;
int main() {
try {
ThreadedExecutor executor;
for(int i = 0; i < 5; i++)
executor.execute(new LiftOff(10, i));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
Note that in some cases a single Executor can be used to create and manage all the threads in your system. You must still place the threading code inside a try block because an Executor’s execute( ) function may throw Synchronization_Exceptions if something goes wrong. This is true for any function that involves changing the state of a synchronization object (starting threads, acquiring mutexes, waiting on conditions, etc.), as you will learn later in this chapter.
The program will exit as soon as all the tasks in the Executor complete.
In the previous example, the ThreadedExecutor creates a thread for each task that you want to run, but you can easily change the way these tasks are executed by replacing the ThreadedExecutor with a different type of Executor. In this chapter, using a ThreadedExecutor is fine, but in production code it might result in excessive costs from the creation of too many threads. In that case, you can replace it with a PoolExecutor, which will use a limited set of threads to execute the submitted tasks in paralleclass="underline"
//: C11:PoolExecutor.cpp
//{L} ZThread
#include "zthread/PoolExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;
int main() {
try {
// Constructor argument is minimum number of threads:
PoolExecutor executor(5);
for(int i = 0; i < 5; i++)
executor.execute(new LiftOff(10, i));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
With the PoolExecutor, you do expensive thread allocation once, up front, and the threads are reused when possible. This saves time because you aren’t constantly paying for thread creation overhead for every single task. Also, in an event-driven system, events that require threads to handle them can be generated as quickly as you want by simply fetching them from the pool. You don’t overrun the available resources because the PoolExecutor uses a bounded number of Thread objects. Thus, although this book will use ThreadedExecutors, consider using PoolExecutors in production code.
A ConcurrentExecutor is like a PoolExecutor with a fixed size of one thread. This is useful for anything you want to run in another thread continually (a long-lived task), such as a task that listens to incoming socket connections. It is also handy for short tasks that you want to run in a thread, for example, small tasks that update a local or remote log, or for an event-dispatching thread.
If more than one task is submitted to a ConcurrentExecutor, each task will run to completion before the next task is begun, all using the same thread. In the following example, you’ll see each task completed, in the order that it was submitted, before the next one is begun. Thus, a ConcurrentExecutor serializes the tasks that are submitted to it.
//: C11:ConcurrentExecutor.cpp
//{L} ZThread
#include "zthread/ConcurrentExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;
int main() {
try {
ConcurrentExecutor executor;