Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
Memory and resource management are major concerns in C++.
When you create any C++ program, you have the option of creating objects on the
stack or on the heap (using new). In a single-threaded program, it s
usually easy to keep track of object lifetimes so that you don t try to use
objects that are already destroyed.
The examples shown in this chapter create Runnable
objects on the heap using new, but you ll notice that these objects are
never explicitly deleted. However, you can see from the output when you
run the programs that the thread library keeps track of each task and
eventually deletes it, because the destructors for the tasks are called. This
happens when the Runnable::run( ) member function
completes returning from run( ) indicates that the task is
finished.
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 {RunByHand}
// Destroying objects while threads are still
// running will cause serious problems.
//{L} ZThread
#include <iostream>
#include "zthread/Thread.h"
#include "zthread/ThreadedExecutor.h"
using namespace ZThread;
using namespace std;
class Count {
enum { 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 a 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 <iostream>
#include "zthread/Thread.h"
#include "zthread/CountedPtr.h"
using namespace ZThread;
using namespace std;
class Count {
enum { 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.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |