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
The previous example shows a fundamental problem when using threads: You never know when a thread might be run. Imagine sitting at a table with a fork, about to spear the last piece of food on your plate, and as your fork reaches for it, the food suddenly vanishes (because your thread was suspended and another task came in and stole the food). That’s the problem you’re dealing with when writing concurrent programs.
Sometimes you don’t care if a resource is being accessed at the same time you’re trying to use it (the food is on some other plate). But for multithreading to work, you need some way to prevent two threads from accessing the same resource, at least during critical periods.
Preventing this kind of collision is simply a matter of putting a lock on a resource when one thread is using it. The first thread that accesses a resource locks it, and then the other threads cannot access that resource until it is unlocked, at which time another thread locks and uses it, and so on. If the front seat of the car is the limited resource, the child who shouts "Dibs!" acquires the lock.
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
To solve the problem in the above program, we identify the
//: C11:MutexEvenGenerator.cpp
// Preventing thread collisions with mutexes.
//{L} ZThread
#include "EvenChecker.h"
#include "zthread/ThreadedExecutor.h"
#include "zthread/Mutex.h"
#include
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
} ///:~
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
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.
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: