Thus, we need to be able to prevent any other tasks from accessing the storage when that storage is not in a proper state. That is, we need to have a mechanism that excludes a second task from accessing the storage when a first task is already using it. This idea is fundamental to all multithreading systems and is called mutual exclusion; the mechanism used abbreviates this to mutex. The ZThread library contains a mutex mechanism declared in the header Mutex.h.
To solve the problem in the above program, we identify the critical sections in which mutual exclusion must apply; then we acquire the mutex before entering the critical section and release it at the end of the critical section. Only one thread can acquire the mutex at any time, so mutual exclusion is achieved:
//: C11:MutexEvenGenerator.cpp
// Preventing thread collisions with mutexes.
//{L} ZThread
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/Mutex.h"
#include <iostream>
using namespace ZThread;
using namespace std;
class MutexEvenGenerator : public Generator {
int currentEvenValue;
Mutex lock;
public:
MutexEvenGenerator() { currentEvenValue = 0; }
~MutexEvenGenerator() {
cout << "~MutexEvenGenerator" << endl;
}
int nextValue() {
lock.acquire();
currentEvenValue++;
Thread::yield();
currentEvenValue++;
int rval = currentEvenValue;
lock.release();
return rval;
}
};
int main() {
EvenChecker::test<MutexEvenGenerator>();
} ///:~
The only changes here are in MutexEvenGenerator, which adds a Mutex called lock and uses acquire( ) and release( ) in nextValue( ). Note that nextValue( ) must capture the return value inside the critical section, because if you return from inside the critical section, you won’t release the lock and will thus prevent it from being acquired again. (This usually leads to deadlock, which you’ll learn about at the end of this chapter.)
The first thread that enters nextValue( ) acquires the lock, and any further threads that try to acquire the lock are blocked from doing so until the first thread releases the lock. At that point, the scheduling mechanism selects another thread that is waiting on the lock. This way, only one thread at a time can pass through the code that is guarded by the mutex.
Simplified coding with Guards
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 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 on the local stack that acquire( ) a Lockable object when constructed and release( ) that lock when destroyed. Because the Guard object exists on the local stack, it will automatically be destroyed regardless of how the function exits and will therefore always unlock the Lockable object. Here’s the above example reimplemented using Guards:
//: C11:GuardedEvenGenerator.cpp
// Simplifying mutexes with the Guard template.
//{L} ZThread
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
#include <iostream>
using namespace ZThread;
using namespace std;
class GuardedEvenGenerator : public Generator {
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 abort:
//: 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.