Win32 Thread Synchronization, Part 2: Helper Classes

Introduction

This article is a continuation of the Thread Synchronization Helper Classes article. Part 1 contained a general discussion of Win32 threading, talked about typical problems with sharing data between threads, and offered some solutions using critical sections and mutexes. You also saw some typical pitfalls that often occur when using these Win32 synchronization objects.

Part 2 introduces thread synchronization helper classes, their implementation, and includes sample projects.

Synchronization Library: Overview and Purpose

This library provides implementations for critical section, mutex, and single write, multiple reader classes. In addition, it also provides an autolock class to provide auto lock and unlock for the synchronization classes.

The classes described herein are designed to help reduce the common programming errors associated with natively using critical sections and mutexes as discussed in Part 1.

Library Classes

The critical section and mutex classes are merely thin wrappers to the Win32 APIs that handle synchronization object initialization/creation, locking, unlocking, and cleanup. Proper object initialization and cleanup is ensured during class construction and destruction. Each class implements two public Lock and Unlock methods that call the underlying lock and unlock native APIs appropriate for each object type.

Critical section class

The CLockableCS class is a thin critical section wrapper that performs critical section initialization in the constructor and cleanup in the destructor. The lock and unlock public methods can be called directly or implicitly by using the CAutolockT class.

class CLockableCS
{
// Methods
public:
   // Ctor / Dtor
   inline CLockableCS() throw()  { InitializeCriticalSection( &m_CS ); }
   inline ~CLockableCS() throw() { DeleteCriticalSection( &m_CS ); }

   // Lock / Unlock
   inline void Lock() throw()    { EnterCriticalSection( &m_CS ); }
   inline void Unlock() throw()  { LeaveCriticalSection( &m_CS ); }

// Attributes
private:
   CRITICAL_SECTION m_CS;        // Internal Critical Section
};

Mutex class

The CLockableMutex class is a thin mutex wrapper that creates a named mutex in the constructor and performs cleanup in the destructor. The lock method takes a timeout parameter to specify how long to wait to acquire the lock. If the lock method errors or timesout before a lock is acquired, the class throws CAutoLockExc or CAutoLockTimeoutExc exceptions. The lock and unlock public methods can be called directly or implicitly by using the CAutoLockT class.

class CLockableMutex
{
public:
   inline CLockableMutex( LPCTSTR szName ) throw()
      : m_hMutex( ::CreateMutex( NULL, FALSE, szName ) )
   {
   }

   inline ~CLockableMutex() throw()
   {
      if(m_hMutex) ::CloseHandle( m_hMutex );
   }

   inline void Lock( DWORD dwTimeout )
   {
      DWORD dwWaitResult = ::WaitForSingleObject( m_hMutex, dwTimeout );
      switch( dwWaitResult )
      {
      case WAIT_OBJECT_0:
         break;
      case WAIT_TIMEOUT:
         throw CAutoLockTimeoutExc( dwTimeout );
         break;
      default:
         throw CAutoLockExc( dwWaitResult, ::GetLastError() );
      }
   }

   inline void Unlock() throw()
   {
      ::ReleaseMutex( m_hMutex );
   }

private:
   HANDLE m_hMutex;              // Mutex Handle
};

Single writer, multiple reader class

The single writer, multiple reader (SWMR) class is used for cases where there are infrequent writes, but concurrent read operations. For this type of usage, the CLockableSWMR class will be more performant than using the critical section class because the CLockableCS doesn't distinguish between reading and writing and locks for both, whereas the SWMR class only locks for write operations, so multiple read threads can still access simultaneously. The CLockableSWMR class is for single process use only. The source for the CLockableSWMR class is several pages long. In the interest of saving space, please refer to the source listing in the AutoLock.h file located in the article's source code.

CAutoLockT class

The CAutoLockT class is a templated class used to provide scope-based locking/unlocking for any of the CLockableXXX classes as well as any class derived from the CComObjectRoot ATL class.

template<class T>
class CAutoLockT
{
// Attributes
private:
   T* m_pObject;                 // the locked object

// Ctor/dtor
public:
   // Critical Section Ctor
   inline CAutoLockT( T* pObject ) throw()
      : m_pObject( pObject )
   {
      ATLASSERT( NULL != pObject );
      m_pObject->Lock();
   }

    // Mutex Ctor
   inline CAutoLockT( T* pObject, DWORD dwTimeout )
      : m_pObject(pObject)
   {
      ATLASSERT( NULL != pObject );
      m_pObject->Lock( dwTimeout );
   }

   // SRMR Ctor
   inline CAutoLockT( T* pObject,
                      const _LOCKTYPE uLockType,
                      LPCTSTR szOperation = NULL ) throw()
                      : m_pObject(pObject)
   {
      ATLASSERT( NULL != pObject );
      m_pObject->Lock( uLockType, szOperation );
   }

   // Ctor
   inline ~CAutoLockT()
   {
      m_pObject->Unlock();
   }
};

Win32 Thread Synchronization, Part 2: Helper Classes

Lockable class usage

The lock classes can be used either as base classes or as embedded class members.

Base class examples

Each of the CLockable-derived classes can be used as base classes.

// Class derivation of a critical section (Used when sharing a
// class instance between threads)
class CFoo : public CLockableCS
{
}

// Using the CS lockable base class with STL (Used when sharing a
// STL queue between threads)
class CLockQueue : public std::queue<CMyClass*>,
   public CLockableCS
{
}

// Mutex derived class (This pattern may be used if the class
// contains data shared between processes)
class CBar : public CLockableMutex
{
public:
// Mutex name declared in ctor
   CBar() : CLockableMutex( _T("CBarMutexName") ) {}
}
Embedded class examples

Any of the classes can be embedded and used directly in a consuming class.

// Embedded example class
class CEmbedded
{
// Attributes
private:
   CLockableCS       m_csListenSink;       // Critical section
   CLockableMutex    m_mtxCrossProcess;    // Mutex
CLockableSWMR        m_swmrMachines;       // SWMR
}

Using the CLockableCS Class

The usage for the CLockableCS class is simple: Declare a CLockableCS variable and lock it with the CAutoLockT class when you want to protect memory shared between threads.

SlowCopy revisited

In Part 1, you initially looked at the original SlowCopy example that illustrated a string shared between two threads without any form of synchronization that resulted in string corruption. Next, you looked at the example compiled with the CSlowCopyNativeCS class that added synchronization using native critical section classes; this eliminated the corruption seen in the original example. In your final look, you will compile the example using the CSlowCopyAutoLockCS class; this will replace the native critical section calls with CLockableCS and CAutoLockT class calls.

Compiling the source

To change the SlowCopy example to synchronize using the CAutoLockT and CLockableCS helper classes, simply comment out the CSlowCopyNativeCS line and uncomment the CSlowCopyAutoLockCS line in the _tmain function. The function should look similar to the following:

int _tmain(int argc, _TCHAR* argv[])
{
   {
      // CSlowCopyNoSync sc;
      // CSlowCopyNativeCS sc;
      CSlowCopyAutoLockCS sc;

      sc.PrintHeader();
      sc.CreateDisplayThread();
      sc.PerformCopy();
      sc.PrintFooter();
   }
   return 0;
}
Program description

When compiled with CSlowCopyAutoLockCS, the SlowCopy program runs the same as the CSlowCopyNativeCS class, except that the explicit native Win32 critical section calls are replaced with the CAutoLockT and CLockableCS helper class calls.

To protect data using the helper classes, yoy need to make the following code changes:

  1. Declare a CLockableCS class member variable (m_csDestStrGuard).
  2. Use the CAutoLockT<CLockableCS> class to protect the SlowStrCopy method in the primary thread.
  3. Use the CAutoLockT<CLockableCS> class to protect the DisplayThread method in the secondary thread.
Source code listing

The following listing shows the two virtual functions that use the helper classes to provide the synchronization. Notice that the Lock and Unlock methods of the CLockableCS class are not explicitly called. Instead, you use the CAutoLockT class to provide scope-based locking and unlocking.

virtual LPTSTR SlowStrCpy( LPTSTR szDest, LPCTSTR szSource )
{
   // Lock the CLockableCS object to prevent the secondary
   // thread access to the string resource 
   // NOTE: CAutoLockT will automatically unlock the lock
   // object on method exit
   CAutoLockT< CLockableCS > lock( &m_csDestStrGuard );

   LPTSTR szStart = szDest;
   while( *szSource != _T('\0') )
   {
      *szDest++ = *szSource++;

      // To illustrate what happens when a thread gets pre-empted
      // before its work has completed, we are going to copy a char
      // and then give up our remaining time slice. This will cause
      // our secondary thread to display the string.

      Sleep( 0 );
   }

   *szDest = _T('\0');

   return szStart;
}


virtual UINT WINAPI DisplayThread( )
{
   int nLineNumber = 1;

   while( TRUE )
   {
      {  // Autolock scope
         // Prevent access to the dest string from the primary thread
         // while the string is displayed
         CAutoLockT< CLockableCS > lock( &m_csDestStrGuard );

         std::cout
            << setiosflags( std::ios::right )
            << std::setw(3)
            << nLineNumber++
            << _T(") ")
            << m_szDest
            << std::endl;

      }   // End of autolock scope (m_csStringGuard auto-unlocked here)

      // Check whether the shutdown event has been set; if so,
      // exit the thread
      if( WAIT_OBJECT_0 == WaitForSingleObject( m_hShutdownEvent, 0 ) )
      {
         return 0;
      }
   }

   // In this example, we never reach here
   return 1;
}
Program output

The program output for SlowCopy example running the CSlowCopyAutoLockCS class is similar to the example running the native critical section code.

------------------------------------------------------------------
------------------------------------------------------------------
SlowCopyAutoLockCS Example - Illustrates synchronizing shared memory
between two threads using the CAutoLockT and CLockableCS helper classes.

Source: The source string that will replace the dest string.
Dest: Original destination string that will be overwritten.

Secondary Thread Started
1) Original destination string that will be overwritten.
2) Original destination string that will be overwritten.
3) Original destination string that will be overwritten.
4) The source string that will replace the dest string.
Secondary Thread Exited

Destination string after SlowCopyAutoLockCS:
The source string that will replace the dest string.

End of program!
------------------------------------------------------------------
------------------------------------------------------------------

Program interpretation

The program output between the native critical section code and the synchronization classes are nearly identical; this is to be expected—after all, the synchronization classes are merely wrappers to the native APIs, so one wouldn't expect the output to differ.

Win32 Thread Synchronization, Part 2: Helper Classes

Using the CLockableMutex Class

The CLockableMutex class is similar to using the CLockableCS class with one difference: Lock operations must be wrapped in a try/catch block that captures CAutoLockTimeoutExc and CAutoLockWaitExc exceptions.

Log send/receive example

Program description

This example is actually two programs, LogSnd and LogRcv, that use a memory mapped file, protected by a mutex, to share data between two applications. To simulate an app sending log data to a receiver application, LogSnd creates a std::queue of log items and a secondary thread that removes each item from the queue, copies it into the memory mapped file, and signals the LogRcv application that there is an available log item. The LogRcv application's purpose in life is to wait for log items to arrive and display them to the console.

Compiling the source

To compile the example, first unzip the accompanying zipped source file and Open the ThreadSync.sln solution in VC.NET 2003. Set the 'LogSnd' project to the 'Active' project by right-clicking on the 'LogSnd' node and choosing the 'Set As Startup Project' item in the popup menu. Next, compile the project and press 'F5' to execute it.

Source code listing

The source listing for the LogSnd and LogRcv samples is a bit large to fit into the text, so I am just including the mutex locking portion of the LogRcv code here. For the complete source listing, please refer to the sample projects.

try
{
   CAutoLockT< CLockableMutex > lock( &m_mtxMMFile,
                                      MUTEX_LOCK_TIMEOUT );
   LPLOGITEM pLI = reinterpret_cast<LPLOGITEM>(m_pData);

   std::cout
   << _T("\tLogItem Event - Depth: ")
      << pLI->uDepth
      << _T("\tMsg: ")
      << W2T(pLI->wszMsg);

   SetEvent( m_hLogItemReceivedEvent );
}
catch( CAutoLockTimeoutExc )
{
   std::cout
   << _T("\tProcessLogItems() Failed with Lock Timeout")
      << std::endl;

   return HRESULT_FROM_WIN32( WAIT_TIMEOUT );
}
catch( CAutoLockWaitExc& e )
{
   hr = HRESULT_FROM_WIN32( e.GetLastError() );

   std::cout
   << _T("\tProcessLogItems() Failed with Error: ")
      << std::hex
      << hr
             << std::endl;
}
Program output

------------------------------------------------------------------
------------------------------------------------------------------

LogSnd - Illustrates uses a mutex to guard memory shared
between two processes.

------------------------------------------------------------------
------------------------------------------------------------------

LogRcv - Illustrates uses a mutex to guard memory shared
between two processes.

LogItem Event - Depth: 0 Msg: Log message: 0
LogItem Event - Depth: 1 Msg: Log message: 1
...
...
...
LogItem Event - Depth: 998 Msg: Log message: 998
LogItem Event - Depth: 999 Msg: Log message: 999
End of LogRcv program!
------------------------------------------------------------------
------------------------------------------------------------------
End of LogSnd program!
------------------------------------------------------------------
------------------------------------------------------------------
Press any key to continue

Program interpretation

The console output for this project is a bit unusual because it's actually two projects outputted into the same console. So, you are seeing the LogSnd application start and launch the LogRcv application. It is from the LogRcv application where the log items are received and displayed to the console. There are two 'End of Program' statements because after the LogSnd application sends all it's log items to LogRcv, it signals LogRcv to exit.

Only one example

Program description

This example uses the CLockableMutex class to limit an MFC dialog application to a single instance. To make the example have the functionality one would expect in a full-featured application, when a user invokes a second instance, the second instance will bring the first instance to the foreground before exiting. A memory-mapped file is used to pass the dialog hWnd between process instances.

Compiling the source

Open the ThreadSync.sln solution in VC.NET 2003 and set the 'OnlyOne' project to the 'Active' project by right-clicking on the 'OnlyOne' node and choosing the 'Set As Startup Project' item in the popup menu. Next, compile the project.

Source code listing

Because you are using the CLockableMutex helper class, the actual code to implement the mutex is quite small—only three lines long. In fact, the majority of the code that modifies this MFC dialog application is code to set up the memory-mapped file used pass the main dialog hWnd of the first instance around to multiple processes. To make this even simpler, I've created the CSingleInstance class to contain the functionality as listed below.

//+----------------------------------------------------------------
// Class:   CSingleInstance
//
// Purpose: Simple class used to limit a process to a single instance
//
// Comment: NOTE: When the SetFirstInstanceHwnd method is called, the
//          first program instance will be brought to the foreground
//          when a user attempts to instantiate a second instance.
//
//+----------------------------------------------------------------
class CSingleInstance
{
public:

   // Ctor/Dtor
   CSingleInstance( LPCTSTR szName )
   : m_mtxSingleInstance( szName )
      , m_hMMFile( NULL )
      , m_pData( NULL ) 
      , m_sMMFName( szName + CString( _T("MMF") ) )
      , m_bFirstInstanceHwndSet( FALSE )
   {
   }

   ~CSingleInstance()
   {
      if( NULL != m_hMMFile )
      {
         ::UnmapViewOfFile( m_pData );
         ::CloseHandle( m_hMMFile );
      }
   }

// Implementation
public:
   //+-------------------------------------------------------------
   // Method:   CheckFirstInstance
   // Purpose:  Checks if this is the first instance of the
   //           application. If not the first instance, it uses the
   //           hWnd of the first instance stored in the memory
   //           mapped file to bring the first instance to the
   //           foreground.
   //
   // Params:   void
   // Return:   S_OK    - First Application Instance
   //           S_FALSE - Application already exists (not 1st instance)
   //           E_xxxx  - HRESULT win32 errors (see CreateFileMapping
   //                     and MapViewOfFile)
   //+-------------------------------------------------------------
   HRESULT CheckFirstInstance( )
   {
      HRESULT hr = S_OK;

      // Create the Memory Mapped File to share the main dialog Hwnd
      if( FAILED( hr = CreateMMF( ) ) )
      {
         return hr;
      }
      
      // Check if the mutex exists already. If it does, it means
      // there is already an instance of the app running.
      if( m_mtxSingleInstance.AlreadyExists( ) )
      {
         // Retrieve the first instance main dialog's hWnd and
         // use it to bring the first instance into the foreground

         // Verify that SetFirstInstanceHwnd method has been called.
         // We check this param because users may not always want
         // to activate (perform SetForegroundWindow on the first
         // instance). If the user does not call
         // SetFirstInstanceHwnd, the program will still be limited
         // to a single instance, but the first instance will not be
         // brought into the foreground.
         if( TRUE == m_bFirstInstanceHwndSet )
         {
            HWND* phWnd = reinterpret_cast<HWND*>(m_pData);
            if( NULL != phWnd )
            {
               ::SetForegroundWindow( *phWnd );
            }
         }
         return S_FALSE;
      }
      
      return hr;
   }

   //+-------------------------------------------------------------
   // Method:     SetFirstInstanceHwnd
   // Purpose:    Stores the hWnd of main dialog (first instance) into
   //             the memory mapped file. This hWnd is used to bring
   //             the first instance to the foreground when a second
   //             instance is opened.
   //
   // Params:     HWND to main dialog of first instance
   // Return:     void
   //
   //+-------------------------------------------------------------
   void SetFirstInstanceHwnd( HWND hWnd )
   {
      ASSERT( NULL != m_pData ); // MMF must be created first
        
      HWND* phWndMMF = reinterpret_cast<HWND*>(m_pData);
      *phWndMMF = hWnd;

      m_bFirstInstanceHwndSet = TRUE;
   }
private:
   //+-------------------------------------------------------------
   // Method:     CreateMMF
   // Purpose:    Creates a memory mapped file used to store
   //             the hwnd of the main dialog.
   //
   // Params:     void
   // Return:     S_OK   - Success
   //             E_xxxx - Returns HRESULT win32 errors (see
   //                      CreateFileMapping and MapViewOfFile)
   //+-------------------------------------------------------------
   HRESULT CreateMMF()
   {
      if( NULL != ( m_hMMFile = CreateFileMapping( INVALID_HANDLE_VALUE,
                                                   NULL,
                                                   PAGE_READWRITE,
                                                   0,
                                                   sizeof( INT_PTR ),
                                                   m_sMMFName )))
      {
         if( NULL !=
            (m_pData = (LPBYTE) MapViewOfFile(m_hMMFile,
                                              FILE_MAP_READ
                                              | FILE_MAP_WRITE,
                                              0,
                                              0,
                                              sizeof( INT_PTR )) ) )
      {
         *m_pData = NULL;
         return S_OK;
      }
   }

   return HRESULT_FROM_WIN32( ::GetLastError( ) );
   }

// Attributes
private:
   CLockableMutex  m_mtxSingleInstance;      // Mutex used to restrict
                                             // the app to a single
                                             // instance
   HANDLE          m_hMMFile;                // Memory Mapped File
                                             // Handle
   PBYTE           m_pData;                  // Raw pointer to MMFile
                                             // data.
// This pointer will be usedto store the hWnd of the
// first dialog instance.
   CString         m_sMMFName;               // Memory Mapped File Name
   BOOL            m_bFirstInstanceHwndSet;  // Used to track if the
                                             // hWnd has been set.
};

Win32 Thread Synchronization, Part 2: Helper Classes

Using the CLockableSWMR Class

The CLockableSWMR usage is similar to using the CLockableCS class except that a second parameter is passed to the CAutoLockT class to distinguish between obtaining a lock to read or to write.

Source code listing

Rather than including a complete example for this single writer, multiple reader lock class, I will take the easy way out and just include a few lines of code to illustrate how to lock to read and lock to write.

Locking to read
CAutoLock< CLockableSWMR_Derived > lock( &m_SWMR, LT_WAITTOREAD );
Locking to write
CAutoLock< CLockableSWMR_Derived > lock( &m_SWMR, LT_WAITTOWRITE );

Helper Class Benefits

Reduce programming errors

As mentioned in Part 1, many of the errors associated with programming the synchronization objects are related to forgetting to release and destroy the objects properly. Through the use of the helper classes, in combination with the CAutoLockT class, many of the typical programming errors can be avoided with the added benefit of smaller, more readable code. This is because the helper classes ensure proper synchronization initialization and cleanup and the CAutoLockT ensures unlock is always called even when the program encounters an exception.

More compact code

If you count the lines of code necessary to perform the initialization, locking, unlocking, and cleanup of the critical section code in the CSlowCopyNativeCS class, you would find that there are seven explicit lines of code needed compared with three lines of code when using the CLockableCS code in CSlowCopyAutoLockCS class. When discussing 'explicit' lines of code, you are talking about the actual lines the developer has to write.

Table 1 shows a significant reduction in explicit lines of code. Although the same number of API calls is required to use the critical section, the helper classes reduce the number of explicit API calls and allow the code to be more compact. This source code reduction can be significant for complex applications with multiple threads and many shared resources.

Table 1: Critical Section Code Comparison: Native Win32 vs. Helper Classes
Operation Native Win32 Helper Classes
Declaration CRITICAL_SECTION cs; CLockableCS m_CS;
Initialization InitializeCriticalSection(&cs); [Implicit]
Locking EnterCriticalSection(&cs); CAutoLockT<CLockableCS> lock(&m_CS);
Unlocking LeaveCriticalSection(&cs); [Implicit]
Cleanup DeleteCriticalSection(&cs); [Implicit]

Summary

In Part 1, you learend about processes, threads, and thread scheduling. You also learned about using the Win32 synchronization objects, saw a few code examples describing the general problem of data sharing between threads, and were given a solution using the native synchronization APIs. Finally, you learned about several potential problems using these APIs. In Part 2, you saw the synchronization class helpers and samples to demonstrate their usage. These classes help to overcome the common problems associated with using the synchronization APIs directly.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • The explosion in mobile devices and applications has generated a great deal of interest in APIs. Today's businesses are under increased pressure to make it easy to build apps, supply tools to help developers work more quickly, and deploy operational analytics so they can track users, developers, application performance, and more. Apigee Edge provides comprehensive API delivery tools and both operational and business-level analytics in an integrated platform. It is available as on-premise software or through …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds