Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
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 <iostream>
#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 a Synchronization_Exception 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 parallel:
//: C11:PoolExecutor.cpp
//{L} ZThread
#include <iostream>
#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 <iostream>
#include "zthread/ConcurrentExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;
int main() {
try {
ConcurrentExecutor executor;
for(int i = 0; i < 5; i++)
executor.execute(new LiftOff(10, i));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
Like a ConcurrentExecutor, a SynchronousExecutor is used when you want only one task at a time to run, serially instead of concurrently.
Unlike ConcurrentExecutor, a SynchronousExecutor doesn t create
or manage threads on it own. It uses the thread that submits the task and thus
only acts as a focal point for synchronization. If you have n threads
submitting tasks to a SynchronousExecutor, no two tasks are ever run at
once. Instead, each one is run to completion, then the next one in the queue is
begun.
For example, suppose you have a number of threads running
tasks that use the file system, but you are writing portable code so you don t
want to use flock( ) or another OS-specific call to lock a file. You can run these tasks with a SynchronousExecutor to ensure
that only one task at a time is running from any thread. This way, you don t
need to deal with synchronizing on the shared resource (and you won t clobber
the file system in the meantime). A better solution is to synchronize on the
resource (which you ll learn about later in this chapter), but a SynchronousExecutor
lets you skip the trouble of getting coordinated properly just to prototype
something.
//: C11:SynchronousExecutor.cpp
//{L} ZThread
#include <iostream>
#include "zthread/SynchronousExecutor.h"
#include "LiftOff.h"
using namespace ZThread;
using namespace std;
int main() {
try {
SynchronousExecutor executor;
for(int i = 0; i < 5; i++)
executor.execute(new LiftOff(10, i));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
When you run the program, you ll see that the tasks are
executed in the order they are submitted, and each task runs to completion
before the next one starts. What you don t see is that no new threads are
created the main( ) thread is used for each task, since in this
example, that s the thread that submits all the tasks. Because SynchronousExecutor
is primarily for prototyping, you may not use it much in production code.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |