Win32 Synchronization Classes
This small library of classes defines higher level synchronization for on top of the Windows NT synchronization primitives.
One of the problems with writing multithreaded code is the subtlety of thread synchronization. Some projects have died slow, agonizing deaths because the implemented thread synchronization functioned most of the time.
The bad news about synchronization is that you can't really get it part right and put in a few exceptions to handle the rest. The code is either right or it's wrong. After a particularly difficult multithreaded application I had to work on, I built these C++ classes so that I would not need to code these details again.
In February, 1998, I got a chance to clean up my synchronization classes for Windows NT. The file SynchTools.zip contains the versions of the classes written at that time. The SynchTools classes seem complete. I have added a new high-level tool (the Barrier class) and cleaned up the test code.
The original classes are described below.
- The Synchronizer class waits on a mutex at constructor time and releases it at destructor time. (The Synchronizer appears to have vanished during one of my ISP moves. If I find it again, it will be restored.)
- Interesting template class which generalizes the functionality of the Synchronizer.
The bounded buffer classes
- Implementation file for BoundedBuffer, BBConsumer, and BBProducer.
- Interface file for BoundedBuffer, BBConsumer, and BBProducer.
The reader/writer classes
- Implementation file for ReadWriteProtector, RWPReader, and RWPWriter.
- Interface file for ReadWriteProtector, RWPReader, and RWPWriter.
- Test harness for the ReaderWriter classes.
- Test harness for the BoundedBuffer classes.
Mutexes are probably the easiest to understand of the synchronization primitives. Only one thread can have the mutex at a time. When you need the item protected by the mutex, you attempt to acquire the mutex. When you are finished with the protected item, you release the mutex.
The problem normally occurs when either the programmer forgets to acquire the mutex or forgets to release it. Of the two problems, the most common seems to be forgetting to release the mutex. This normally occurs when the programmer allows an exit from the mutex-protected code which does not pass through the release.
The Synchronizer class ensures that acquires and releases are matched. In fact since the release is performed by the destructor, we are even protected from exceptions which could possibly bypass the most carefully constructed logic.
Microsoft implemented a similar class in their MFC framework. However, the class presented here is simpler and only applies to mutexes. By attempting to handle several types of waitable objects, the MFC Lock classes become somewhat unwieldy. I also had difficulty getting them to work correctly.
Bounded Buffer problem
A good use for threads is in constructing a Producer/Consumer pattern. Some problems separate nicely into a thread for producing or finding raw data and a thread for consuming or processing the data. Quite often it is necessary to place some form of buffer between the two threads to handle temporary excesses in production.
In the best possible case, a producer thread (P) should be able to package its data up and hand it to the buffer without any knowledge of the consumer. Likewise the consumer thread (C) should just request data from the buffer without any knowledge of the producer thread.
The problem normally occurs when the thread C wants data that has not yet been supplied by thread P, or when thread P has produced more data than C can handle. Assuming a finite buffer between the two threads, we can build a simple mechanism for dealing with these problems.
Whenever thread C requires data, it should make a request from the buffer. If there is no data available, thread C should block until new data becomes available. Whenever thread P has data for the buffer, it should insert the data into the buffer. If the buffer is full, thread P should block until space becomes available in the buffer.
The BoundedBuffer class and its two helper classes, BBProducer and BBConsumer, implement the synchronization for this problem. For each buffer you wish to protect create a BoundedBuffer object specifying the maximum number of items the buffer can hold.
Any time you need to add something to the buffer, create a BBProducer object on the stack. When you need to extract something from the buffer, create a BBConsumer item on the stack. In addition to handling the empty and full buffer problems, these classes implement mutual exclusion on the buffer so that no thread can interrupt before you finish adding or removing an item from the buffer.
Another interesting multithreading problem involves a resource that is read by many threads and updated by others. A good example of this problem would involve an in-memory database which is periodically updated from some other source.
Since most of the threads accessing the system are only reading, mutual exclusion is too strict. Two threads can read the same area of memory without conflicting. When we get ready to write to the area, we must make sure that no threads are currently reading it. Moreover, as long as we are writing to the area no new reader threads can be allowed access until we finish.
The Readers/Writer problem is a great example of a synchronization problem that most people get wrong. It is actually easy to set up a set of protections that work most of the time. With only a little bit more work, we can set up a system that works fine as long as we don't mind the writer thread being locked out of the system indefinitely. Lastly, even if you get all of this right, there are a few subtleties in getting it to work with more than one writer thread.
For each resource you want to protect, create one ReadWriteProtector object. Any time you want to read the resource, create a RWPReader object on the stack. Any time you need to write to the resource, create a RWPWriter object on the stack.