Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
Consider the following example where one task generates even numbers and other tasks consume those numbers. Here, the only job of the
consumer threads is to check the validity of the even numbers.
We ll first define EvenChecker, the consumer
thread, since it will be reused in all the subsequent examples. To decouple EvenChecker
from the various types of generators that we will experiment with, we ll create
an interface called Generator, which contains the minimum necessary
functions that EvenChecker must know about: that it has a nextValue( )
function and that it can be canceled.
//: C11:EvenChecker.h
#ifndef EVENCHECKER_H
#define EVENCHECKER_H
#include <iostream>
#include "zthread/CountedPtr.h"
#include "zthread/Thread.h"
#include "zthread/Cancelable.h"
#include "zthread/ThreadedExecutor.h"
class Generator : public ZThread::Cancelable {
bool canceled;
public:
Generator() : canceled(false) {}
virtual int nextValue() = 0;
void cancel() { canceled = true; }
bool isCanceled() { return canceled; }
};
class EvenChecker : public ZThread::Runnable {
ZThread::CountedPtr<Generator> generator;
int id;
public:
EvenChecker(ZThread::CountedPtr<Generator>&
g, int ident)
: generator(g), id(ident) {}
~EvenChecker() {
std::cout << "~EvenChecker "
<< id << std::endl;
}
void run() {
while(!generator->isCanceled()) {
int val = generator->nextValue();
if(val % 2 != 0) {
std::cout << val << " not
even!" << std::endl;
generator->cancel(); // Cancels all
EvenCheckers
}
}
}
// Test any type of generator:
template<typename GenType> static void test(int
n = 10) {
std::cout << "Press Control-C to
exit" << std::endl;
try {
ZThread::ThreadedExecutor executor;
ZThread::CountedPtr<Generator> gp(new
GenType);
for(int i = 0; i < n; i++)
executor.execute(new
EvenChecker(gp, i));
} catch(ZThread::Synchronization_Exception& e)
{
std::cerr << e.what() << std::endl;
}
}
};
#endif // EVENCHECKER_H ///:~
The Generator class introduces the abstract Cancelable class, which is part of the ZThread library. The goal of Cancelable
is to provide a consistent interface to change the state of an object via
the cancel( ) function and to see whether the object has been
canceled with the isCanceled( ) function. Here, we use the simple
approach of a bool canceled flag, similar to the quitFlag
previously seen in ResponsiveUI.cpp. Note that in this example the class
that is Cancelable is not Runnable. Instead, all the EvenChecker
tasks that depend on the Cancelable object (the Generator) test
it to see if it s been canceled, as you can see in run( ). This
way, the tasks that share the common resource (the Cancelable Generator)
watch that resource for the signal to terminate. This eliminates the so-called race condition, where two or more tasks race to respond to a condition and
thus collide or otherwise produce inconsistent results. You must be careful to
think about and protect against all the possible ways a concurrent system can
fail. For example, a task cannot depend on another task because task shutdown
order is not guaranteed. Here, by making tasks depend on non-task objects
(which are reference counted using CountedPtr) we eliminate the
potential race condition.
In later sections, you ll see that the ZThread library
contains more general mechanisms for termination of threads.
Since multiple EvenChecker objects may end up sharing
a Generator, the CountedPtr template is used to reference count
the Generator objects.
The last member function in EvenChecker is a static
member template that sets up and performs a test of any type of Generator
by creating one inside a CountedPtr and then starting a number of EvenCheckers
that use that Generator. If the Generator causes a failure, test( )
will report it and return; otherwise, you must press Control-C to terminate it.
EvenChecker tasks constantly read and test the values
from their associated Generator. Note that if generator->isCanceled( )
is true, run( ) returns, which tells the Executor in EvenChecker::test( )
that the task is complete. Any EvenChecker task can call cancel( )
on its associated Generator, which will cause all other EvenCheckers
using that Generator to gracefully shut down.
The EvenGenerator is simple nextValue( )
produces the next even value:
//: C11:EvenGenerator.cpp
// When threads collide.
//{L} ZThread
#include <iostream>
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
using namespace ZThread;
using namespace std;
class EvenGenerator : public Generator {
unsigned int currentEvenValue; // Unsigned can t
overflow
public:
EvenGenerator() { currentEvenValue = 0; }
~EvenGenerator() { cout <<
"~EvenGenerator" << endl; }
int nextValue() {
++currentEvenValue; // Danger point here!
++currentEvenValue;
return currentEvenValue;
}
};
int main() {
EvenChecker::test<EvenGenerator>();
} ///:~
It s possible for one thread to call nextValue( )
after the first increment of currentEvenValue and before the second (at
the place in the code commented Danger point here! ), which puts the value into
an incorrect state. To prove that this can happen, EvenChecker::test( )
creates a group of EvenChecker objects to continually read the output of
an EvenGenerator and test to see if each one is even. If not, the error
is reported and the program is shut down.
This program may not detect the problem until the EvenGenerator
has completed many cycles, depending on the particulars of your operating
system and other implementation details. If you want to see it fail much
faster, try putting a call to yield( ) between the first and second
increments. In any event, it will eventually fail because the EvenChecker
threads are able to access the information in EvenGenerator while it s
in an incorrect state.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |