Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
The use of mutexes rapidly becomes complicated when
exceptions are introduced. To make sure that the mutex is always released, you must ensure that each possible exception path includes a call to release( ).
In addition, any function that has multiple return paths must carefully ensure
that it calls release( ) at the appropriate points.
These problems can be easily solved by using the fact that a
stack-based (auto) object has a destructor that is always called regardless of
how you exit from a function scope. In the ZThread library, this is implemented
as the Guard template. The Guard template creates objects that acquire( )
a Lockable object when constructed and release( ) that
lock when destroyed. Guard objects created on the local stack will
automatically be destroyed regardless of how the function exits and will always
unlock the Lockable object. Here s the above example reimplemented using
Guards:
//: C11:GuardedEvenGenerator.cpp {RunByHand}
// Simplifying mutexes with the Guard template.
//{L} ZThread
#include <iostream>
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;
using namespace std;
class GuardedEvenGenerator : public Generator {
unsigned int currentEvenValue;
Mutex lock;
public:
GuardedEvenGenerator() { currentEvenValue = 0; }
~GuardedEvenGenerator() {
cout << "~GuardedEvenGenerator"
<< endl;
}
int nextValue() {
Guard<Mutex> g(lock);
++currentEvenValue;
Thread::yield();
++currentEvenValue;
return currentEvenValue;
}
};
int main() {
EvenChecker::test<GuardedEvenGenerator>();
} ///:~
Note that the temporary return value is no longer necessary
in nextValue( ). In general, there is less code to write, and the
opportunity for user error is greatly reduced.
An interesting feature of the Guard template is that
it can be used to manipulate other guards safely. For example, a second Guard
can be used to temporarily unlock a guard:
//: C11:TemporaryUnlocking.cpp
// Temporarily unlocking another guard.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;
class TemporaryUnlocking {
Mutex lock;
public:
void f() {
Guard<Mutex> g(lock);
// lock is acquired
// ...
{
Guard<Mutex, UnlockedScope> h(g);
// lock is released
// ...
// lock is acquired
}
// ...
// lock is released
}
};
int main() {
TemporaryUnlocking t;
t.f();
} ///:~
A Guard can also be used to try to acquire a lock for a
certain amount of time and then give up:
//: C11:TimedLocking.cpp
// Limited time locking.
//{L} ZThread
#include "zthread/Thread.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;
class TimedLocking {
Mutex lock;
public:
void f() {
Guard<Mutex, TimedLockedScope<500> >
g(lock);
// ...
}
};
int main() {
TimedLocking t;
t.f();
} ///:~
In this example, a Timeout_Exception will be thrown
if the lock cannot be acquired within 500 milliseconds.
Synchronizing entire
classes
The ZThread library also provides a GuardedClass
template to automatically create a synchronized wrapper for an entire class. This means that every member function in the class will automatically be guarded:
//: C11:SynchronizedClass.cpp {-dmc}
//{L} ZThread
#include "zthread/GuardedClass.h"
using namespace ZThread;
class MyClass {
public:
void func1() {}
void func2() {}
};
int main() {
MyClass a;
a.func1(); // Not synchronized
a.func2(); // Not synchronized
GuardedClass<MyClass> b(new MyClass);
// Synchronized calls, only one thread at a time
allowed:
b->func1();
b->func2();
} ///:~
Object a is a not synchronized, so func1( )
and func2( ) can be called at any time by any number of threads.
Object b is protected by the GuardedClass wrapper, so each member
function is automatically synchronized and only one function per object can be
called any time.
The wrapper locks at a class level of granularity, which may
affect performance. If
a class contains some unrelated functions, it may be better to synchronize
those functions internally with two different locks. However, if you find
yourself doing this, it means that one class contains groups of data that may
not be strongly associated. Consider breaking the class into two classes.
Guarding all member functions of a class with a mutex does
not automatically make that class thread-safe. You must carefully consider all
threading issues in order to guarantee thread safety.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |