Creating Thread-Safe Datatypes and Inverse Semaphores

[Previous] [Next]

One day I was writing some code and reached a point where I needed a kernel object whose behavior was opposite that of a semaphore object. I needed the object to be signaled when its current resource count was 0 and nonsignaled when its current resource count was greater than 0.

I could see many uses for this type of object. For example, you might have a thread that needs to wake up after you execute some operation 100 times. To pull this off, you would need a kernel object that you could initialize to 100. When the kernel object's count is greater than 0, the object should not be signaled. Each time you execute some operation, you would want to decrement the count in the kernel object. When the count reaches 0, the object should be signaled so that your other thread can wake up to process something. This is a common problem, and I don't understand why Windows doesn't offer such a built-in primitive.

Actually, Microsoft could easily solve this problem by allowing a semaphore's current resource count to be negative. You could initialize the semaphore's count to -99 and then call ReleaseSemaphore after each operation. When the semaphore's count reached 1, 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 expect them to change this code in the foreseeable future.

In this section, I'll present a set of C++ template classes that have the behavior of an inverse semaphore plus a whole lot more. The code for these classes is in the Interlocked.h file. (See Figure 10-2.)

When I first set out to tackle this problem, I realized that a thread-safe way to manipulate a variable was at the heart of the solution. I wanted to design an elegant solution that would make code that references the variable trivially easy to write. Obviously, the easiest way to make a resource thread-safe is to protect it with a critical section. Using C++, it is fairly easy to endow a data object with thread safety. All you 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, and in the destructor you call DeleteCriticalSection. For all the other member variables you call EnterCriticalSection, you manipulate the variable, and then you call LeaveCriticalSection. If you implement a C++ class this way, it is easy to write code that accesses a data structure in a thread-safe fashion. This is the founding principle of all the C++ classes I present in this section. (Of course, I could have used the optex presented in the previous section instead of critical sections.)

The first class is a resource guard class called CResGuard. It contains a CRITICAL_SECTION data member and a LONG data member. The LONG data member keeps 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 only a single thread can create an object, a C++ object's constructor and destructor do not have to be thread-safe. The IsGuarded member function simply returns whether EnterCriticalSection has been called at least once against this object. As I said before, this is for debugging purposes. Placing a CRITICAL_SECTION inside a C++ object ensures that the critical section is properly initialized and deleted.

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 a destructor. The constructor calls the CResGuard's Guard member function, which calls EnterCriticalSection, and the CGuard's destructor calls the CResGuard's Unguard member function, which calls LeaveCriticalSection. Setting up these classes this way makes it easy to manipulate a CRITICAL_SECTION. Here is small code fragment that uses these classes:

 struct SomeDataStruct { ... } g_SomeSharedData; // Create a CResGuard object that protects g_SomeSharedData. // Note: The constructor initializes the critical section and // the destructor deletes the critical section. CResGuard g_rgSomeSharedData; void AFunction() { // This function touches the shared data. // Protect the resource from being accessed from multiple threads. CResGuard::CGuard gDummy(g_rgSomeSharedData); //Enter critical section // Touch the g_SomeSharedData resource. ... } // Note: LeaveCriticalSection is called when gDummy goes out of scope. 

The next C++ class, CInterlockedType, contains all the parts necessary to create a thread-safe data object. I made the CInterlockedType class a template class so that it can be used to make any data type thread-safe. So, 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 template data type that you want to make thread-safe. This data member is private and can be manipulated only using CInterlockedType's member functions. The second data member is an instance of a CResGuard object, which guards access to the data member. The CResGuard object is a protected data member so a class derived from CInterlockedType can easily protect its data.

You are expected to 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 the derived class is responsible for using 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 call. Two GetVal functions return the current value of the data object. In debug builds of the file, both of these functions first check to see whether the data object is guarded. If the object were not guarded, GetVal could return a value for the object and then allow another thread to change the object's value before the original caller examined it.

I assume that a caller is getting the value of the object so that it can change the value in some way. Based on this assumption, the GetVal functions require that the caller have 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 constant data types 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, the derived class's function should guard access to the data and then call the SetVal function. Like the GetVal functions, the SetVal function 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 whether the data value is actually changing. If it is, 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. You can use the OnValChanged member function to add some powerful capabilities to the derived class, as you'll see later when we discuss the CWhenZero class.

Thus far, we have talked about a lot of abstract classes and concepts. Now let's see how you can use all of this architecture for the good of all humankind. I present to you the CInterlockedScalar class—a template class derived from CInterlockedType. You can use this class 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 does not 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 0 so our object is always constructed in a known state. CInterlockedScalar's destructor does nothing at all.

All of CInterlockedScalar's remaining member functions change the scalar value. One member function exists 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. The member functions are simple, so I won't explain them in any detail; you can examine the code to see what they do. However, I'll show you how to use these classes. The following code declares a thread-safe BYTE and manipulates it:

 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 as simple as manipulating a non-thread-safe scalar. In fact, the code is identical, thanks to C++'s 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 variable with only 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 whose behavior was the opposite of a semaphore's. The C++ class that offers this behavior is my CWhenZero class. The CWhenZero class is derived from the CInterlockedScalar class. When the scalar value is 0, the CWhenZero object is signaled; when the data value is not 0, the CWhenZero object is not signaled. This is the opposite behavior of a semaphore.

As you know, C++ objects cannot be signaled; only kernel objects can be signaled and used for thread synchronization. So a CWhenZero object must contain some additional data members, which are handles to 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 0, and m_hevtNotZero, a handle to an event kernel object that is signaled when the data value is not 0.

CWhenZero's constructor accepts an initial value for the data object and also lets you specify whether these two event kernel objects should be manual-reset (the default) or auto-reset. The constructor then calls CreateEvent to create the two event kernel objects and set them to the signaled or nonsignaled state, depending on whether the data's initial value is 0. 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? The CWhenZero class overrides this virtual function. This function is responsible for keeping the event kernel objects signaled or nonsignaled 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 whether the new value is 0, and if so, it sets the m_hevtZero event and resets the m_hevtNotZero event. If the new value is not 0, OnValChanged does the reverse.

Now, when you want a thread to wait until the data value is 0, all you have to do is the following:

 CWhenZero<BYTE> b = 0; // A thread-safe BYTE // Returns immediately because b is 0 WaitForSingleObject(b, INFINITE); b = 5; //Returns only if another thread sets b to 0 WaitForSingleObject(b, INFINITE); 

You can write the call to WaitForSingleObject as I did on the preceding page because the CWhenZero class also includes a cast operator member function that casts a CWhenZero object to a kernel object HANDLE. In other words, if you pass a CWhenZero C++ object to any function that expects a HANDLE object, this cast operator function gets called and its return value is passed to the function. CWhenZero's HANDLE cast operator function returns the handle of the m_hevtZero event kernel object.

The m_hevtNotZero event handle inside the CWhenZero class lets you write code that waits for the data value to not be 0. 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 have 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 WaitForSingleObject(b.GetNotZeroHandle(), INFINITE); b = 0; // Returns only if another thread sets b to not 0 WaitForSingleObject(b.GetNotZeroHandle(), INFINITE); 

The InterlockedType Sample Application

The InterlockedType ("10 InterlockedType.exe") application, shown in Figure 10-2, tests the C++ classes I just described. The source code and resource files for the application are in the 10-InterlockedType directory on this book's companion CD-ROM. I always run the application in the debugger so I can closely watch all the class member functions and variables change.

The code shows a common programming scenario that goes like this: A thread spawns several worker threads and then initializes a block of memory. Then 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. Then the main thread reinitializes the memory block with new data and wakes up the worker threads to start the whole process all over again.

By looking at the code, you can see how trivial it is to solve this common programming problem with readable and maintainable C++ code. As you can see, the CWhenZero class gives us a whole lot more than the opposite behavior of a semaphore. We now have a thread-safe number, which is signaled when its value is 0! You can increment and decrement a semaphore's value, but you can add, subtract, multiply, divide, modulo, set explicitly to any value, or even perform bit operations with a CWhenZero object! A CWhenZero object is substantially more powerful than a semaphore kernel object.

It is fun to come up with ideas for these C++ template classes. For example, you can create a CInterlockedString class derived from the CInterlockedType class. You can use the CInterlockedString class to manipulate a character string in a thread-safe fashion. Then you can derive a CWhenCertainString class from your CInterlockedString class, which signals an event kernel object when the character string becomes a certain value or values. The possibilities are endless.

Figure 10-2. The InterlockedType sample application

IntLockTest.cpp

 /****************************************************************************** Module: IntLockTest.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> #include "Interlocked.h" /////////////////////////////////////////////////////////////////////////////// // Set to TRUE when worker threads should terminate cleanly. volatile BOOL g_fQuit = FALSE; /////////////////////////////////////////////////////////////////////////////// DWORD WINAPI WorkerThread(PVOID pvParam) { CWhenZero<BYTE>& bVal = * (CWhenZero<BYTE> *) pvParam; // Should worker thread terminate? while (!g_fQuit) { // Wait for something to do WaitForSingleObject(bVal.GetNotZeroHandle(), INFINITE); // If we should quit, quit if (g_fQuit) continue; // Do something chMB("Worker thread: We have something to do"); bVal—; // We're done // Wait for all worker threads to stop WaitForSingleObject(bVal, INFINITE); } chMB("Worker thread: terminating"); return(0); } /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { // Initialize to indicate that NO worker threads have anything to do CWhenZero<BYTE> bVal = 0; // Create the worker threads const int nMaxThreads = 2; HANDLE hThreads[nMaxThreads]; for (int nThread = 0; nThread < nMaxThreads; nThread++) { DWORD dwThreadId; hThreads[nThread] = CreateThread(NULL, 0, WorkerThread, (PVOID) &bVal, 0, &dwThreadId); } int n; do { // Do more work or stop running? n = MessageBox(NULL, TEXT("Yes: Give worker threads something to do\nNo: Quit"), TEXT("Primary thread"), MB_YESNO); // Tell worker threads that we're quitting if (n == IDNO) InterlockedExchangePointer((PVOID*) &g_fQuit, (PVOID) TRUE); bVal = nMaxThreads; // Wake the worker threads if (n == IDYES) { // There is work to do, wait for the worker threads to finish WaitForSingleObject(bVal, INFINITE); } } while (n == IDYES); // There is no more work to do, the process wants to die. // Wait for the worker threads to terminate WaitForMultipleObjects(nMaxThreads, hThreads, TRUE, INFINITE); // Close the worker thread handles. for (nThread = 0; nThread < nMaxThreads; nThread++) CloseHandle(hThreads[nThread]); // Tell the user that the process is dying chMB("Primary thread: terminating"); return(0); } //////////////////////////////// End of File ////////////////////////////////// 

 Interlocked.h /****************************************************************************** Module: Interlocked.h Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #pragma once /////////////////////////////////////////////////////////////////////////////// // Instances of this class will be accessed by multiple threads. So, // all members of this class (except the constructor and destructor) // must be thread-safe. class CResGuard { public: CResGuard() { m_lGrdCnt = 0; InitializeCriticalSection(&m_cs); } ~CResGuard() { DeleteCriticalSection(&m_cs); } // IsGuarded is used for debugging BOOL IsGuarded() const { return(m_lGrdCnt > 0); } public: class CGuard { public: CGuard(CResGuard& rg) : m_rg(rg) { m_rg.Guard(); }; ~CGuard() { m_rg.Unguard(); } private: CResGuard& m_rg; }; private: void Guard() { EnterCriticalSection(&m_cs); m_lGrdCnt++; } void Unguard() { m_lGrdCnt—; LeaveCriticalSection(&m_cs); } // Guard/Unguard can only be accessed by the nested CGuard class. friend class CResGuard::CGuard; private: CRITICAL_SECTION m_cs; long m_lGrdCnt; // # of EnterCriticalSection calls }; /////////////////////////////////////////////////////////////////////////////// // Instances of this class will be accessed by multiple threads. So, // all members of this class (except the constructor and destructor) // must be thread-safe. template <class TYPE> class CInterlockedType { public: // Public member functions // Note: Constructors & destructors are always thread-safe CInterlockedType() { } CInterlockedType(const TYPE& TVal) { m_TVal = TVal; } virtual ~CInterlockedType() { } // Cast operator to make writing code that uses // thread-safe data type easier operator TYPE() const { CResGuard::CGuard x(m_rg); return(GetVal()); } protected: // Protected function to be called by derived class TYPE& GetVal() { chASSERT(m_rg.IsGuarded()); return(m_TVal); } const TYPE& GetVal() const { assert(m_rg.IsGuarded()); return(m_TVal); } TYPE SetVal(const TYPE& TNewVal) { chASSERT(m_rg.IsGuarded()); TYPE& TVal = GetVal(); if (TVal != TNewVal) { TYPE TPrevVal = TVal; TVal = TNewVal; OnValChanged(TNewVal, TPrevVal); } return(TVal); } protected: // Overridable functions virtual void OnValChanged( const TYPE& TNewVal, const TYPE& TPrevVal) const { // Nothing to do here } protected: // Protected guard for use by derived class functions mutable CResGuard m_rg; private: // Private data members TYPE m_TVal; }; /////////////////////////////////////////////////////////////////////////////// // Instances of this class will be accessed by multiple threads. So, // all members of this class (except the constructor and destructor) // must be thread-safe. template <class TYPE> class CInterlockedScalar : protected CInterlockedType<TYPE> { public: CInterlockedScalar(TYPE TVal = 0) : CInterlockedType<TYPE>(TVal) { } ~CInterlockedScalar() { /* Nothing to do */ } // C++ does not allow operator cast to be inherited. operator TYPE() const { return(CInterlockedType<TYPE>::operator TYPE()); } TYPE operator=(TYPE TVal) { CResGuard::CGuard x(m_rg); return(SetVal(TVal)); } TYPE operator++(int) { // Postfix increment operator CResGuard::CGuard x(m_rg); TYPE TPrevVal = GetVal(); SetVal((TYPE) (TPrevVal + 1)); return(TPrevVal); // Return value BEFORE increment } TYPE operator—(int) { // Postfix decrement operator. CResGuard::CGuard x(m_rg); TYPE TPrevVal = GetVal(); SetVal((TYPE) (TPrevVal - 1)); return(TPrevVal); // Return value BEFORE decrement } TYPE operator += (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() + op)); } TYPE operator++() { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() + 1)); } TYPE operator -= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() - op)); } TYPE operator—() { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() - 1)); } TYPE operator *= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() * op)); } TYPE operator /= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() / op)); } TYPE operator %= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() % op)); } TYPE operator ^= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() ^ op)); } TYPE operator &= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() & op)); } TYPE operator |= (TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() | op)); } TYPE operator <<=(TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() << op)); } TYPE operator >>=(TYPE op) { CResGuard::CGuard x(m_rg); return(SetVal(GetVal() >> op)); } }; /////////////////////////////////////////////////////////////////////////////// // Instances of this class will be accessed by multiple threads. So, // all members of this class (except the constructor and destructor) // must be thread-safe. template <class TYPE> class CWhenZero : public CInterlockedScalar<TYPE> { public: CWhenZero(TYPE TVal = 0, BOOL fManualReset = TRUE) : CInterlockedScalar<TYPE>(TVal) { // The event should be signaled if TVal is 0 m_hevtZero = CreateEvent(NULL, fManualReset, (TVal == 0), NULL); // The event should be signaled if TVal is NOT 0 m_hevtNotZero = CreateEvent(NULL, fManualReset, (TVal != 0), NULL); } ~CWhenZero() { CloseHandle(m_hevtZero); CloseHandle(m_hevtNotZero); } // C++ does not allow operator= to be inherited. TYPE operator=(TYPE x) { return(CInterlockedScalar<TYPE>::operator=(x)); } // Return handle to event signaled when value is zero operator HANDLE() const { return(m_hevtZero); } // Return handle to event signaled when value is not zero HANDLE GetNotZeroHandle() const { return(m_hevtNotZero); } // C++ does not allow operator cast to be inherited. operator TYPE() const { return(CInterlockedScalar<TYPE>::operator TYPE()); } protected: void OnValChanged(const TYPE& TNewVal, const TYPE& TPrevVal) const { // For best performance, avoid jumping to // kernel mode if we don't have to if ((TNewVal == 0) && (TPrevVal != 0)) { SetEvent(m_hevtZero); ResetEvent(m_hevtNotZero); } if ((TNewVal != 0) && (TPrevVal == 0)) { ResetEvent(m_hevtZero); SetEvent(m_hevtNotZero); } } private: HANDLE m_hevtZero; // Signaled when data value is 0 HANDLE m_hevtNotZero; // Signaled when data value is not 0 }; //////////////////////////////// End of File ////////////////////////////////// 

 InterlockedType.rc //Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_INTERLOCKEDTYPE ICON DISCARDABLE "InterLockedType.ICO" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED 



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net