Burdening the thread with deleting a task is a problem. That thread doesn’t necessarily know if another thread still needs to make a reference to that Runnable, and so the Runnable may be prematurely destroyed. To deal with this problem, tasks in ZThreads are automatically reference-counted by the ZThread library mechanism. A task is maintained until the reference count for that task goes to zero, at which point the task is deleted. This means that tasks must always be deleted dynamically, and so they cannot be created on the stack. Instead, tasks must always be created using new, as you see in all the examples in this chapter.
Often you must also ensure that non-task objects stay alive as long as tasks need them. Otherwise, it’s easy for objects that are used by tasks to go out of scope before those tasks are completed. If this happens, the tasks will try to access illegal storage and will cause program faults. Here’s a simple example:
//: C11:Incrementer.cpp
// Destroying objects while threads are still
// running will cause serious problems.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/ThreadedExecutor.h"
#include <iostream>
using namespace ZThread;
using namespace std;
class Count {
static const int sz = 100;
int n[sz];
public:
void increment() {
for(int i = 0; i < sz; i++)
n[i]++;
}
};
class Incrementer : public Runnable {
Count* count;
public:
Incrementer(Count* c) : count(c) {}
void run() {
for(int n = 100; n > 0; n--) {
Thread::sleep(250);
count->increment();
}
}
};
int main() {
cout << "This will cause a segmentation fault!" << endl;
Count count;
try {
Thread t0(new Incrementer(&count));
Thread t1(new Incrementer(&count));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
The Count class may seem like overkill at first, but if n is only a single int (rather than an array), the compiler can put it into a register and that storage will still be available (albeit technically illegal) after the Count object goes out of scope. It’s difficult to detect the memory violation in that case. Your results may vary depending on your compiler and operating system, but try making it n single int and see what happens. In any event, if Count contains an array of ints as above, the compiler is forced to put it on the stack and not in a register.
Incrementer is a simple task that uses a Count object. In main( ), you can see that the Incrementer tasks are running for long enough that the Count object will go out of scope, and so the tasks try to access an object that no longer exists. This produces a program fault.
To fix the problem, we must guarantee that any objects shared between tasks will be around as long as those tasks need them. (If the objects were not shared, they could be composed directly into the task’s class and thus tie their lifetime to that task.) Since we don’t want the static program scope to control the lifetime of the object, we put the object on the heap. And to make sure that the object is not destroyed until there are no other objects (tasks, in this case) using it, we use reference counting.
Reference counting was explained thoroughly in volume one of this book and further revisited in this volume. The ZThread library includes a template called CountedPtr that automatically performs reference counting and deletes an object when the reference count goes to zero. Here’s the above program modified to use CountedPtr to prevent the fault:
//: C11:ReferenceCounting.cpp
// A CountedPtr prevents too-early destruction.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/CountedPtr.h"
#include <iostream>
using namespace ZThread;
using namespace std;
class Count {
static const int sz = 100;
int n[sz];
public:
void increment() {
for(int i = 0; i < sz; i++)
n[i]++;
}
};
class Incrementer : public Runnable {
CountedPtr<Count> count;
public:
Incrementer(const CountedPtr<Count>& c ) : count(c) {}
void run() {
for(int n = 100; n > 0; n--) {
Thread::sleep(250);
count->increment();
}
}
};
int main() {
CountedPtr<Count> count(new Count);
try {
Thread t0(new Incrementer(count));
Thread t1(new Incrementer(count));
} catch(Synchronization_Exception& e) {
cerr << e.what() << endl;
}
} ///:~
Incrementer now contains a CountedPtr object, which manages a Count. In main( ), the CountedPtr objects are passed into the two Incrementer objects by value, so the copy-constructor is called, increasing the reference count. As long as the tasks are still running, the reference count will be nonzero, and so the Count object managed by the CountedPtr will not be destroyed. Only when all the tasks using the Count are completed will delete be called (automatically) on the Count object by the CountedPtr.
Whenever you have objects that are used by more than one task, you’ll almost always need to manage those objects using the CountedPtr template in order to prevent problems arising from object lifetime issues.
Improperly accessing resources
Consider the following example in which one task generates even numbers and other tasks consume those numbers. In this case, 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 "zthread/CountedPtr.h"
#include "zthread/Thread.h"
#include "zthread/Cancelable.h"
#include "zthread/ThreadedExecutor.h"