Code for this article: Win320797.exe (15KB)
Jeffrey Richter wrote Advanced Windows, Third Edition (Microsoft Press, 1997) and Windows 95: A Developer's Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32 programming courses (www.solsem.com). He can be reached at www.JeffreyRichter.com.|
Q I need a kernel object that works opposite the way that a Win32® semaphore kernel object works: the object should be signaled when its count is zero and not signaled when its count is greater than zero. I anticipate many uses for this type of object. For example, I have a thread that needs to wake up after I have executed an operation 100 times. To pull this off, I want a kernel object that I can initialize to 100. When the kernel object's count is greater than zero, the object should not be signaled. Each time I execute an operation, I want to decrement the count in the kernel object. When the count reaches zero, the object should be signaled so that my other thread can wake up to process something. I think this is a common problem and that the operating system should provide such a primitive. Does Win32 offer a kernel object that does what I want?
If not, how can I design something that does what I just described?
A I agree with you; this is a common programming problem and there should be a kernel object that has the reverse behavior of a semaphore. But unfortunately, there is not. Microsoft could make some small modifications to Windows NT® and Windows® 95 so that semaphore objects could accomplish the behavior you desire. If the semaphore's count is allowed to be negative, you could easily get what you want. You could initialize the semaphore's count to –99, and then call ReleaseSemaphore after each operation. When the semaphore's count reaches one, the object would be signaled and your other thread could wake up to do its processing. Alas, Microsoft prohibits a semaphore's count from being negative, and I don't anticipate this code changing in the foreseeable future.
However, all is not lost. Several months ago I implemented a set of C++ template classes that give you the behavior you're looking for plus a whole lot more. The remainder of this column discusses these classes and how to use them. All of these classes are in the Interlocked.h header file, which you may download from the link at the top of this article.
When I first set out to tackle this problem, I realized that a thread-safe way to manipulate a variable was at
the cornerstone of the solution. I wanted to design an elegant solution that would make code referencing the variable easy to write. Obviously, the easiest way to make a resource thread-safe is to protect that resource with a critical section. Using C++, it's fairly easy to endow a data object with thread-safeness. All you have to do is create a C++ class that contains the variable you want to protect
and a CRITICAL_SECTION data structure. Then, in the constructor you call InitializeCriticalSection, in the destructor you call DeleteCriticalSection, and in all the
other member variables you call EnterCriticalSec-
tion, manipulate the variable, and then call LeaveCriticalSection. If you implement a C++ class this way, it's easy to
write code that accesses a data structure in a thread-safe fashion. This concept is behind all the C++ classes I'll present here.
The first is a resource guard class called CResGuard
(see Figure 1). This class contains a CRITICAL_SECTION data member and a LONG data member. The LONG
data member is used to keep track of how many times the owning thread has entered the critical section. This
information can be useful for debugging. The CResGuard object's constructor and destructor call InitializeCriticalSection and DeleteCriticalSection, respectively. Since a single thread can only create an object, a C++ object's constructor and destructor don't have to be thread-safe. The IsGuarded member function simply returns whether or not EnterCriticalSection has been called at least once against this object. (As I said, this is used for debugging purposes.) Placing a CRITICAL_SECTION inside a C++ object ensures that the critical section is initialized and deleted properly.
The CResGuard class also offers a nested public C++ class: CGuard. A CGuard object contains a reference to a CResGuard object and offers only a constructor and destructor. The constructor calls CResGuard's Guard member function, which calls EnterCriticalSection. CGuard's destructor calls the CResGuard's Unguard member function, which calls LeaveCriticalSection. This way of setting up these classes makes it easy to manipulate a CRITICAL_SECTION. Figure 2 shows a code fragment that uses these classes.
The next C++ class is called CInterlockedType (see Figure 3). This class contains all the parts necessary to create a thread-safe data object. I made CInterlockedType a template class so that it can be used to make any data type thread-safe. For example, you can use this class to make a thread-safe integer, a thread-safe string, or a thread-safe data structure.
Each instance of a CInterlockedType object contains two data members. The first data member is an instance of the templated data type that you want to make thread-safe. It's private and can only be manipulated via CInterlockedType's member functions. The second data member is an instance of a CResGuard object that is used to guard access to the data member. The CResGuard object is a protected data member so that a class derived from CInterlockedType can easily protect its data.
It's expected that you will always derive a new class using the CInterlockedType class as a base class. Earlier, I said that the CInterlockedType class provides all of the parts necessary to create a thread-safe object, but it is the responsibility of the derived class to use these parts correctly to access the data value in a thread-safe fashion.
The CInterlockedType class offers only four public functions: a constructor that does not initialize the data value, another constructor that does initialize the data value, a virtual destructor that does nothing, and a cast operator. The cast operator simply ensures thread-safe access to the data value by guarding the resource and returning the object's current value. (The resource is automatically unguarded when the local variable x goes out of scope.) The cast operator makes it easy to examine the data object's value contained in the class in a thread-safe fashion.
The CInterlockedType class also offers three nonvirtual protected functions that a derived class will want to call. There are two GetVal functions that return the current value of the data object. In debug builds of the file, both of these functions first check to see if the data object is guarded. If the object isn't guarded, it would be possible to return a value for the object and then have the object change its value (by another thread) before the caller examined the value.
I assume that a caller is getting the value of the object so it can change the value in some way. Because of this assumption, the GetVal functions require that the caller has guarded access to the data value. If the GetVal functions determine that the data is guarded, the current value of the data is returned. The two GetVal functions are identical except that one of them operates on a constant version of the object. The two versions allow you to write code that works with both constant and nonconstant data types without the compiler generating warnings.
The third nonvirtual protected member function is SetVal. When a derived class's member function wants to modify the data value, it should guard access to the data and then call SetVal. Like the GetVal functions, SetVal first performs a debug check to make sure that the derived class's code didn't forget to guard access to the data value. Then, SetVal checks to see if the data value is actually changing. If it is changing, SetVal saves the old value, changes the object to its new value, and then calls a virtual, protected member function, OnValChanged, which is passed the old and new data values. The CInterlockedType class implements an OnValChanged member function, which does nothing. The OnValChanged member function allows me to add some powerful capabilities to my derived class, as you'll see later when I discuss the CWhenZero class.
Thus far, I've shown you a lot of abstract classes and concepts. Now it's time to see how all of this architecture can be used for the good of mankind. I now present my CInterlockedScalar classa template class derived from CInterlockType (see Figure 4). The CInterlockedScalar class allows you to create a thread-safe scalar data type such as a byte, a character, a 16-bit integer, a 32-bit integer, a 64-bit integer, a floating point value, and so on. Because the CInterlockedScalar class is derived from the CInterlockedType class, it doesn't have any data members of its own. CInterlockedScalar's constructor simply calls CInterlockedType's constructor, passing an initial value for the scalar. Since the CInterlockedScalar class always works with numeric values, I set the default constructor parameter to zero so that the object is always constructed in a known state. CInterlockedScalar's destructor is very simple and does nothing at all.
All of CInterlockedScalar's remaining member functions change the scalar value. There is one member function for each operation that can be performed on a scalar value. In order for the CInterlockedScalar class to manipulate its data object in a thread-safe fashion, all of these member functions guard the data value before manipulating it. Since the member functions are simple I won't detail them here; you can examine the code to see what they do.
However, I will show you how to use these classes. The following code demonstrates how to declare a thread-safe BYTE and how to manipulate this BYTE:
CInterlockedScalar<BYTE> b = 5; // A thread-safe BYTE
BYTE b2 = 10; // A non-thread-safe BYTE
b2 = b++; // b2=5, b=6
b *= 4; // b=24
b2 = b; // b2=24, b=24
b += b; // b=48
b %= 2; // b=0
Manipulating a thread-safe scalar value is just as simple as manipulating a scalar that isn't thread-safe. In fact, the code is identical thanks to C++ operator overloading! With the C++ classes I've shown you so far, you can easily turn any non-thread-safe variable into a thread-safe one with small changes to your source code.
I had a specific destination in mind when I started designing all these classes: I wanted to create an object that works opposite the way a Win32 semaphore works. The C++ class that offers this behavior is my CWhenZero class (see Figure 5). CWhenZero is derived from the CInterlockedScalar class. When the scalar value is zero, the CWhenZero object is signaled; when the data value is not zero, the CWhenZero object is not signaled. This is the opposite behavior of a Win32 semaphore.
As you know, C++ objects cannot be signaled; only Win32 kernel objects can be signaled and used for thread synchronization. So a CWhenZero object must contain some additional data members, which are handles to Win32 event kernel objects. A CWhenZero object contains two data members: m_hevtZero, a handle to an event kernel object that is signaled when the data value is zero, and m_hevtNotZero, another handle to an event kernel object that is signaled when the data value is not zero.
CWhenZero's constructor accepts an initial value for the data object and also allows you to specify whether these two event kernel objects should be manual-reset (the default) or autoreset. The constructor then calls CreateEvent to create the two event kernel objects and sets them either to the signaled state or nonsignaled state depending on whether the data's initial value is zero. CWhenZero's destructor is merely responsible for closing the two event handles. Because CWhenZero's class publicly inherits the CInterlockedScalar class, all of the overloaded operator member functions are available to users of a CWhenZero object.
Remember the OnValChanged protected member function declared inside the CInterlockedType class? CWhenZero overrides this virtual function, which is responsible for keeping the event kernel objects signaled or not signaled based on the value of the data object. Whenever the data value changes, OnValChanged is called. CWhenZero's implementation of this function checks to see if the new value is zero. If so, it sets the m_hevtZero event and resets the m_hevtNotZero event. If the new value is not zero, OnValChanged does the reverse.
Now, when you want a thread to suspend itself until the data value is zero, just do the following:
CWhenZero<BYTE> b = 0; // A thread-safe BYTE
INFINITE); // Returns immediately
// because b is 0
b = 5;
INFINITE); // Returns only if
// another thread sets b
// to 0
You can write the call to WaitForSingleObject because CWhenZero also includes a cast operator member function that casts a CWhenZero object to a Win32 kernel object HANDLE. In other words, if you pass a CWhenZero C++ object to any function that expects a Win32 HANDLE object, this cast operator function gets called and its return value is what gets passed to the function. CWhenZero's HANDLE cast operator function returns the handle of the m_hevtZero event kernel object. Be aware that the data value may no longer be zero by the time WaitForSingle-Object returns.
The m_hevtNotZero event handle inside the CWhenZero class allows you to write code that waits for the data value to not be zero. Unfortunately, I already have a HANDLE cast operator so I can't have another one that returns the m_hevtNotZero handle. So to get at this handle I added the GetNotZeroHandle member function. Using this function, I can write the following code:
CWhenZero<BYTE> b = 5; // A thread-safe BYTE
// Returns immediately because b is not 0
b = 0;
// Returns only if another thread sets b to not 0
I wrote the IntLockTest program to test all these classes (see Figure 6). The code demonstrates a common programming scenario that goes like this: a thread spawns several worker threads and then initializes a block of memory. After initializing the memory block, the main thread wakes the worker threads so that they can start processing
the memory block. At this point, the main thread must suspend itself until all the worker threads have finished. After all the worker threads have finished, the main thread reinitializes the memory block with new data and then wakes the worker threads to start the process all over again.|
By looking at the code, you can see how easy it is to solve this common programming problem with readable and maintainable C++ code. The CWhenZero class gives you a whole lot more than behavior that's opposite what you get from a Win32 semaphore. You now have a thread-safe number that is signaled when its value is zero! A semaphore allows you to increment its value and decrement its value, but a CWhenZero object allows you to add, subtract, multiply, divide, modulo, set it explicitly to any value, or even perform bit operations on it! This is substantially more powerful than a Win32 semaphore kernel object.
It's fun to come up with more ideas for these C++ template classes. For example, you could create a CInterlockedString class derived from the CInterlockedType class. The CInterlockedString class would allow you to manipulate a character string in a thread-safe fashion. Then you could derive a CWhenCertainString class from your CInterlockedString class, and this would signal an event kernel object when the character string becomes a certain value. The possibilities are endless.
Please let me know if you come up with any more good uses for these classesI'd love to hear about them.
Have a question about programming in Win32?
Send your questions via email to Jeffrey Richter from his website at http://www.jeffreyrichter.com.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.