Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

How to create a Simple Lock Framework for C++ Synchronization

4.88/5 (31 votes)
22 Aug 2013MIT16 min read 141.9K   1.6K  
This article explains how to create your own simple Lock Framework for C++ synchronization.

Table of contents

  1. Introduction
  2. Background
  3. Critical Section vs Mutex vs Semaphore
    1. Critical Section
    2. Mutex
    3. Semaphore
    4. More Info.
  4. Purpose of the Lock Framework
  5. BaseLock Class
  6. Critical Section Class
    1. Constructor/Copy Constructor
    2. Destructor
    3. Lock/TryLock
    4. TryLockFor
    5. Unlock
  7. Semaphore Class
    1. Constructor/Copy Constructor
    2. Destructor
    3. Lock/TryLock/TryLockFor
    4. Unlock
    5. Release
  8. Mutex Class
    1. Constructor/Copy Constructor
    2. Destructor
    3. Lock/TryLock/TryLockFor
    4. Unlock
  9. NoLock Class
    1. Implementation
  10. Auto-release Lock
    1. Implementation
  11. Use-case Examples
    1. Declaring Synchronization Object
    2. Declaring Semaphore Object with initial lock count
    3. Declaring Mutex/Semaphore Object for using across process
    4. Synchronization Usage Type1 (Manual Lock)
    5. Synchronization Usage Type2 (Whole Structure Body Lock)
    6. Synchronization Usage Type3 (TryLock/TryLockFor)
  12. More Practical Example Sources
  13. Conclusion
  14. Reference

Introduction

When developing multi-threaded/multi-process application, the synchronization is one of the big issues. In a case where you have to build your same application for single-threaded/multi-threaded/multi-process environments, then managing the synchronization object is another issue. Having lock (synchronization) framework won't just automatically solve all of the above issues, however it will help finding the way to solve those issues. So this article explains how you can build your own simple lock (synchronization) framework, and how you can use them within your development projects.

Many professional developers might already have their own advanced lock (synchronization) framework, or they might use well-known popular library such as boost library, or Intel TBB. However, sometimes these advanced libraries might be over-kill for your application, and they might not be in your taste. After reading this article, you will be able to build your own lock (synchronization) framework as your taste.

This article will explain the following:

  • BaseLock Class
  • Critical Section Class
  • Semaphore Class
  • Mutex Class
  • NoLock Class
  • Auto-release Lock
  • Use-case examples
** Warning: This article is not about an use-case explanation of an advanced lock framework, nor explanation of differences of synchronization primitives. As stated in the topic, this article only explains "how to build a simple lock framework", and it is your decision to use/change/modify/improve the given framework according to your project and/or development environment.

Background

You have at least understand the concept of below synchronization primitives:

  • Critical Section
  • Semaphore
  • Mutex

and also differences between them.

It is optional, but understanding the MFC Synchronization classes (CSingleLock/CMultiLock, CCriticalSection, CSemaphore, and CMutex) will allow you to implement the lock framework given here with the MFC Synchronization Classes instead of using Windows Default Library functions.

Critical Section vs Mutex vs Semaphore

The information in this section is originally from "Synchronization in Multithreaded Applications with MFC" article, which is written by Arman S.. I just presented information from his article, so people can easily access. So all hard work and contribution should go to Arman S.

Critical Section

Critical sections are working in the user mode unless there is a need to enter the kernel mode. If a thread tries to run a code that is caught be a critical section, it first does a spin blocking and after a specified amount of time, it enters the kernel mode to wait for the critical section. Actually, a critical section consists of a spin counter and a semaphore; the former is for the user mode waiting, and the later is for the kernel mode waiting (sleeping). In Win32 API, there is a CRITICAL_SECTION structure that represents critical section objects. In MFC, there is a class named CCriticalSection. Conceptually, a critical section is a sector of source code that is needed in integrated execution, that is, during the execution of that part of the code it should be guaranteed that the execution will not be interrupted by another thread. Such sectors of code may be required in cases when there is a need to grant a single thread the monopoly of using a shared resource.

Mutex

Mutexes, like critical sections, are designated to protect shared resources from simultaneous accesses. Mutexes are implemented inside the kernel and thus they enter the kernel mode to operate. A mutex can perform synchronization not only between different threads but also between different processes. Such a mutex should have a unique name to be recognized by another process (such mutexes are called named mutexes).

Semaphore

In order to limit the number of threads that use shared resources we should use semaphores. A semaphore is a kernel object. It stores a counter variable to keep track of the number of threads that are using the shared resource. For example, the following code creates a semaphore by the MFC CSemaphore class which could be used to guarantee that only 5 threads at a maximum would be able to use the shared resource in a given time period (this fact is indicated by the first parameter of the constructor). It is supposed that no threads have captured the resource initially (the second parameter):

C++
HANDLE g_sem=CreateSemaphore(NULL,5,5,NULL); 

More Info.

For more information, please read Arms S.'s article here.

Purpose of the Lock Framework

Well-known synchronization primitives in Windows development, are Critical Section, Semaphore, and Mutex. For novice developers, they might spend hard time to understand the concept of synchronization itself, but using different synchronization primitives might be just too much. Therefore the idea of this simple Lock Framework is to let different synchronization primitives to have same interface and use them in same manner, but work as their original purpose during run-time.

BaseLock Class

While different synchronization primitives exist for their own purposes, they still share some basic functionality.

  • Lock
  • TryLock
  • TryLockFor (TryLock for certain period of time)
  • Unlock

Some people might come up with the idea of more functionality, but it will be choice of yours when building your own framework. I will just focus on only those four functionality for the simplicity.

So our BaseLock class will be a pure virtual class, and will be simple as below.

C++
// BaseLock.h
  
class BaseLock
{ 
public:
   BaseLock();
   virtual ~BaseLock();
   virtual bool Lock() =0;
   virtual long TryLock()=0;
   virtual long TryLockFor(const unsigned int dwMilliSecond)=0; 
   virtual void Unlock()=0; 
};

  • Note that Lock function returns boolean for Mutex case, for all other cases, it always returns true. 

Critical Section Class

Since the CRITICAL_SECTION struct already exist, I just named our class as CriticalSectionEx. Our CriticalSectionEx class will be a sub-class of BaseLock class, and also it will be an interface class for CRITICAL_SECTION object.

So our CriticalSectionEx class will be as follow, which will be very similar to our BaseLock class.

// CriticalSectionEx.h
#include "BaseLock.h"

class CriticalSectionEx :public BaseLock
{
public:
   CriticalSectionEx();
   CriticalSectionEx(const CriticalSectionEx& b);
   virtual ~CriticalSectionEx();
   CriticalSectionEx & operator=(const CriticalSectionEx&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
private:
   CRITICAL_SECTION m_criticalSection;
   int m_lockCounter;
};   
  • Note that, the reason that "operator=" returns itself without doing anything is that I didn't want CRITICAL_SECTION object to be replaced by other object. If "operator=" is not implemented, when copy-operator is called, it will automatically call default copy-operator, and the values of CRITICAL_SECTION object will be replaced with other object's value. And the reason, I didn't make it private is that, if copy-operator is private, when some class A holds an CriticalSectionEx object, and when an object of class A tries to copy from other object, it will result an compiler-error.
  • Also, note that, since the Critical Section is re-entrant, and since difference in entering and leaving count will lead to undefined behavior, m_lockCounter is added to track the number of locks/unlocks. (Thanks to Orjan Westin for the suggestion!) 

There are five important functions to know when using Critical Section object in general.

  • InitializeCriticalSection
  • DeleteCriticalSection
  • EnterCriticalSection
  • LeaveCriticalSection
  • TryEnterCriticalSection
For more details of each function's functionality, please refer to MSDN.

Those five functions will be emerged as below,

Constructor/Copy Constructor

C++
// CriticalSectionEx.cpp
CriticalSectionEx::CriticalSectionEx() :BaseLock()
{
   InitializeCriticalSection(&m_criticalSection);
   m_lockCounter=0;
}
CriticalSectionEx::CriticalSectionEx(const CriticalSectionEx& b):BaseLock()
{
   InitializeCriticalSection(&m_criticalSection);
   m_lockCounter=0; 
}     

Above code snippet is implementation of constructor and copy-constructor. By calling "InitializeCriticalSection" within constructor, it is automatically initializing the CRITICAL_SECTION object when the CriticalSectionEx object is created.

  • Also note that, the reason, that copy-constructor is not copying anything but only initializing the CRITICAL_SECTION object as constructor, is same reason as "operator="

Destructor

C++
// CriticalSectionEx.cpp
CriticalSectionEx::~CriticalSectionEx()
{
   assert(m_lockCounter==0);
   DeleteCriticalSection(&m_criticalSection);
}  

If the CRITICAL_SECTION object is initialized, it must be deleted. So by putting "DeleteCriticalSection" in destructor, the CRITICAL_SECTION object will be automatically deleted, when

CriticalSectionEx 
object is deleted/destroyed.

  • Also note, that if m_lockCounter is NOT 0 then it means lock and unlock count are not equal, which might result undefined behavior. 

Lock/TryLock

C++
// CriticalSectionEx.cpp
bool CriticalSectionEx::Lock()
{
   EnterCriticalSection(&m_criticalSection);
   m_lockCounter++
   return true;
}
long CriticalSectionEx::TryLock()
{
   long ret=TryEnterCriticalSection(&m_criticalSection);
   if(ret)
      m_lockCounter++;
   return ret;
}       

This is very straight forward implementation. When "Lock" function is called, it is entering the critical section by calling "EnterCriticalSection" function. And when "TryLock" function is called, it tries to enter the critical section by calling "TryEnterCriticalSection." (Also increment the m_lockCounter for successful locking) 

TryLockFor

C++
// CriticalSectionEx.cpp
long CriticalSectionEx::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0;
   if(ret=TryEnterCriticalSection(&m_criticalSection))
   {
      m_lockCounter++;
      return ret;
   }
   else
   {
      unsigned int startTime,timeUsed;
      unsigned int waitTime=dwMilliSecond;
      startTime=GetTickCount();
      while(WaitForSingleObject(m_criticalSection.LockSemaphore,waitTime)==WAIT_OBJECT_0)
      {
         if(ret=TryEnterCriticalSection(&m_criticalSection))
         {
            m_lockCounter++;
            return ret;
         }
         timeUsed=GetTickCount()-startTime;
         waitTime=waitTime-timeUsed;
         startTime=GetTickCount();
      }
      return 0;
   }
}    
  • Note that, there is no parameter for time in
    TryEnterCriticalSection 
    function originally (to avoid deadlock according to MSDN). So I had to do some trick to simulate the TryLock for certain period of time, however use this with care since there is reason that MS did not implemented this functionality for CRITICAL_SECTION object.
  • CRITICAL_SECTION object, this does NOT guarantee the order of entering the critical section.
What this function does is as the following
  1. Try to enter the critical section using TryEnterCriticalSection
    • If succeeded, return with code from TryEnterCriticalSection
  2. If failed, Wait for the CRITICAL_SECTION object to be released with time given from caller
    • If released, try to enter the Critical Section again using TryEnterCriticalSection.
      • If succeeded, return with code from TryEnterCriticalSection.
      • If failed and if there is time left, repeat step 2 with time left.
      • If failed and if there is no time left, return 0.

Unlock

C++
// CriticalSectionEx.cpp
void CriticalSectionEx::Unlock()
{
   assert(m_lockCounter>=0);
   LeaveCriticalSection(&m_criticalSection);
}   

If the CRITICAL_SECTION object is obtained, it must be released. So by calling "LeaveCriticalSection" within Unlock function, it leaves the critical section, when

Unlock 
function is called.

  • Note that m_lockCounter MUST be greater than or equal to 0, otherwise there is more unlocking count than locking count. 

Semaphore Class

Our Semaphore class will be a sub-class of BaseLock class, and also it will be an interface class for Semaphore object (It is actually a HANDLE).

So our Semaphore class will be as follow, which will be very similar to our BaseLock class.

C++
// Semaphore.h
#include "BaseLock.h"
 
class Semaphore :public BaseLock
{
public:
   Semaphore(unsigned int count=1,const TCHAR *semName=_T(""), LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);
   Semaphore(const Semaphore& b);
   virtual ~Semaphore();
   Semaphore & operator=(const Semaphore&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
   long Release(long count, long * retPreviousCount);
private:
   /// Actual Semaphore
   HANDLE m_sem;
   /// Creation Info
   LPSECURITY_ATTRIBUTES m_lpsaAttributes;
   /// Semaphore Flag
   unsigned int m_count;
};      

  • Note that, the reason that "operator=" returns itself without doing anything is that I didn't want values of Semaphore object to be replaced by other object. If "operator=" is not implemented, when copy-operator is called, it will automatically call default copy-operator, and the values of Semaphore object (such as m_sem, m_lpsaAttributes, and m_count in this case) will be replaced with other object's value. And the reason, I didn't make it private is that, if copy-operator is private, when some class A holds an Semaphore object, and when an object of class A tries to copy from other object, it will result an compiler-error.
  • Also note that, the constructor now takes in parameters such as count, semName, lpsaAttributes where BaseLock and CriticalSectionEx have no parameters for the constructor. This still can be used without the arguments since they have default arguments, and each parameter's detail is as the following:
    • count represent the lock count of the semaphore
      • the default value is "1," which represents the binary semaphore.
    • semName represents the name of the semaphore.
      • the default value is "NULL"
      • This might be needed if the semaphore will be used across the processes. (Please refer to MSDN.)
    • lpsaAttributes represent the security descriptor for new Semaphore.
      • the default value is "NULL," which represents default security descriptor.
      • (For more detail, please refer to MSDN.)

There are four important functions to know when using Semaphore object in general.

  • CreateSemaphore
  • CloseHandle
  • WaitForSingleObject
  • ReleaseSemaphore
For more details of each function's functionality, please refer to MSDN.

Those four functions will be emerged as below,

Constructor/Copy Constructor

C++
// Semaphore.cpp
 
Semaphore::Semaphore(unsigned int count,const TCHAR *semName, LPSECURITY_ATTRIBUTES lpsaAttributes) :BaseLock()
{ 
   m_lpsaAttributes=lpsaAttributes;
   m_count=count;
   m_sem=CreateSemaphore(lpsaAttributes,count,count,semName);
} 
Semaphore::Semaphore(const Semaphore& b) :BaseLock()
{ 
   m_lpsaAttributes=b.m_lpsaAttributes;
   m_count=b.m_count;
   m_sem=CreateSemaphore(m_lpsaAttributes,m_count,m_count,NULL);
}  

Above code snippet is implementation of constructor and copy-constructor. By calling "CreateSemaphore" within constructor, it is automatically creating the Semaphore handle when the Semaphore object is created.

  • Also note that, in Semaphore's case, copy-constructor is copying m_count and m_lpsaAttributes, and creates new Semaphore object. Basically this will have same number of lock count and security descriptor as given Semaphore object, but Semaphore object will be a new instance.

Destructor

C++
// Semaphore.cpp
 
Semaphore::~Semaphore()
{
   CloseHandle(m_sem);
} 

If the Semaphore handle is created, it must be closed. So by calling "CloseHandle" in destructor, the Semaphore handle will be automatically deleted, when Semaphore object is deleted/destroyed.

Lock/TryLock/TryLockFor

// Semaphore.cpp
bool Semaphore::Lock()
{
   WaitForSingleObject(m_sem,INIFINITE);
   return true;
}
long Semaphore::TryLock()
{
   long ret=0;
   if(WaitForSingleObject(m_sem,0) == WAIT_OBJECT_0 )
      ret=1;
   return ret;
}
long Semaphore::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0;
   if( WaitForSingleObject(m_sem,dwMilliSecond) == WAIT_OBJECT_0)
      ret=1;
   return ret;
}    

This is very straight forward implementation.

  • When "Lock" function is called, it waits for the infinite time to obtain the semaphore, when "WaitForSingleObject" returns the semaphore is automatically obtained.
  • When "TryLock" function is called, it tries to obtain the semaphore by waiting for 0 millisecond.
  • When "TryLockFor" function is called, it tries to obtain the semaphore by waiting for as much as given time.
  • For more detail, please refer to MSDN.

Unlock

C++
// Semaphore.cpp
 
void Semaphore::Unlock()
{
   ReleaseSemaphore(m_sem,1,NULL);
} 

If the Semaphore is obtained, it must be released. So by calling "ReleaseSemaphore" within Unlock function, the Semaphore handle can be released when Unlock function is called.

Release  

As Ahmed Charfeddine suggested, for semaphore, "it is important that Unlock would accept a release count parameter and return a success/failure status." Since this is semaphore specific function, you can just extend Semaphore Class by introducing a new function "Release".

C++
// Semaphore.cpp
 
void Semaphore::Release(long count, long * retPreviousCount)
{
   return ReleaseSemaphore(m_sem,count,retPreviousCount); 
}   

This can be used as below:

C++
BaseLock *lock = new Semaphore(10);
...
BOOL ret = dynamic_cast<Semaphore*>(lock)->Release(5);
// OR
Semaphore lock(10);
...
BOOL ret = lock.Release(5);   

(Thanks to Ahmed Charfeddine for the suggestion!)

Mutex Class

Our Mutex class will be a sub-class of BaseLock class, and also it will be an interface class for Mutex object (It is actually a HANDLE).

So our Mutex class will be as follow, which will be very similar to our BaseLock class.

// Mutex.h
#include "BaseLock.h"



class Mutex :public BaseLock
{
public:
   Mutex(const TCHAR *mutexName=NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);
   Mutex(const Mutex& b);
   virtual ~Mutex();
   Mutex & operator=(const Mutex&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
   bool IsMutexAbandoned()
   {
      return m_isMutexAbandoned;
   )
private:
   /// Mutex
   HANDLE m_mutex;
   /// Creation Security Info
   LPSECURITY_ATTRIBUTES m_lpsaAttributes;
   /// Flag for whether this mutex is abandoned or not.
   bool m_isMutexAbandoned;
};     
  • Note that, the reason that "operator=" returns itself without doing anything is that I didn't want values of Mutex object to be replaced by other object. If "operator=" is not implemented, when copy-operator is called, it will automatically call default copy-operator, and the values of Mutex object (such as m_mutex, and m_lpsaAttributes in this case) will be replaced with other object's value. And the reason, I didn't make it private is that, if copy-operator is private, when some class A holds an Mutex object, and when an object of class A tries to copy from other object, it will result an compiler-error.
  • Also note that, the constructor now takes in parameters such as mutexName, lpsaAttributes which is similar to Semaphore's constructor. This still can be used without the arguments since they have default arguments, and each parameter's detail is as the following:
    • mutexName represents the name of the semaphore.
      • the default value is "NULL"
      • This might be needed if the mutex will be used across the applications. (Please refer to MSDN.)
    • lpsaAttributes represent the security descriptor for new Semaphore.
      • the default value is "NULL," which represents default security descriptor.
      • (For more detail, please refer to MSDN.)
  • Note  that, the Mutex can be abandoned. However, I didn't want to change the interface just for Mutex, so I added a function, IsMutexAbandoned to check whether the Mutex is abandoned or not when the locking failed. (Thanks to Orjan Westin for the suggestion.) 

There are four important functions to know when using Mutex object in general.

  • CreateMutex
  • CloseHandle
  • WaitForSingleObject
  • ReleaseMutex
For more details of each function's functionality, please refer to MSDN.

Those four functions will be emerged as below,

Constructor/Copy Constructor

C++
// Mutex.cpp
Mutex::Mutex(const TCHAR *mutexName, LPSECURITY_ATTRIBUTES lpsaAttributes) :BaseLock()
{
   m_isMutexAbandoned=false;
   m_lpsaAttributes=lpsaAttributes;
   m_mutex=CreateMutex(lpsaAttributes,FALSE,mutexName);
}
Mutex::Mutex(const Mutex& b)
{
   m_isMutexAbandoned=false; 
   m_lpsaAttributes=b.m_lpsaAttributes;
   m_mutex=CreateMutex(m_lpsaAttributes,FALSE,NULL);
}    

Above code snippet is implementation of constructor and copy-constructor. By calling "CreateMutex" within constructor, it is automatically creating the Mutex handle when the Mutex object is created.

  • Also note that, in Mutex's case, copy-constructor is copying m_lpsaAttributes, and creates new Mutex object. Basically this will have same security descriptor as given Mutex object, but Mutex object will be a new instance.

Destructor

C++
// Mutex.cpp
 
Mutex::~Mutex()
{
   CloseHandle(m_mutex);
} 

If the Mutex handle is created, it must be closed. So by calling "CloseHandle" in destructor, the Mutex handle will be automatically deleted, when Mutex object is deleted/destroyed.

Lock/TryLock/TryLockFor

C++
// Mutex.cpp
void Mutex::Lock()
{
   bool returnVal = true;
   unsigned long res=WaitForSingleObject(m_mutex,INIFINITE);
   if(res=WAIT_ABANDONED)
   {
      m_isMutexAbandoned=true;
      returnVal=false; 
   }
   return returnVal;
}
long Mutex::TryLock()
{
   long ret=0;
   unsigned long mutexStatus= WaitForSingleObject(m_mutex,0);
   if(mutexStatus== WAIT_OBJECT_0)
      ret=1;
   else if(mutexStatus==WAIT_ABANDONED)
      m_isMutexAbandoned=true;
   return ret;
}
long Mutex::TryLockFor(const unsigned int dwMilliSecond)
{
   long ret=0; 
   unsigned long mutexStatus= WaitForSingleObject(m_mutex,dwMilliSecond);
   if(mutexStatus==WAIT_OBJECT_0)
      ret=1;
   else if(mutexStatus==WAIT_ABANDONED)
      m_isMutexAbandoned=true;
   return ret;
}   

This is very straight forward implementation.

  • When "Lock" function is called, it waits for the infinite time to obtain the mutex, when "WaitForSingleObject" returns the mutex is automatically obtained.
  • When "TryLock" function is called, it tries to obtain the mutex by waiting for 0 millisecond.
  • When "TryLockFor" function is called, it tries to obtain the mutex by waiting for as much as given time.
  • For more detail, please refer to MSDN.
  • Note that for all Locking function, it checks for whether Mutex is abandoned or not, and set the member flag variable, m_isMutexAbandoned, for status. So when the locking failed, you can call "IsMutexAbandoned" function to check whether fail is due to abandoned Mutex

Unlock

C++
// Mutex.cpp
 
void Mutex::Unlock()
{
   ReleaseMutex(m_mutex);
} 

If the Mutex is obtained, it must be released. So by calling "ReleaseMutex" within Unlock function, the Mutex handle can be released when Unlock function is called. 

NoLock Class

NoLock class is like a placeholder for Lock Framework in single-threaded environment. When building same application for multiple environment such as single threaded, multi-threaded, multi-process, NoLock will take the role as placeholder for single-threaded environment. Our Nolock class will be a sub-class of BaseLock class.

So our NoLock class will be as follow, which will be very similar to our BaseLock class.

C++
// NoLock.h
#include "BaseLock.h"
 
class NoLock :public BaseLock
{
public:
   NoLock();
   NoLock(const NoLock& b);
   virtual ~NoLock();
   NoLock & operator=(const NoLock&b)
   {
      return *this;
   }
   virtual bool Lock();
   virtual long TryLock();
   virtual long TryLockFor(const unsigned int dwMilliSecond);
   virtual void Unlock();
private:
};   
  • As explained above, this NoLock class represents only a placeholder, so it does not have any member variable, and also it does not have any lock mechanism functionality.

The implementation will be as below,

Implementation

C++
// NoLock.cpp
NoLock::NoLock() :BaseLock()
{
}
NoLock::NoLock(const NoLock& b):BaseLock()
{
}
NoLock::~NoLock()
{
}
bool NoLock::Lock()
{
   return true;
}
long NoLock::TryLock()
{
   return 1;
}
long NoLock::TryLockFor(const unsigned int dwMilliSecond)
{
   return 1;
}
void NoLock::Unlock()
{
}  

Basically, this NoLock class does not do anything, but just returns true when it is needed.

Auto-release Lock

When developing synchronization, sometimes it is very annoying to match "Lock" and "Unlock." So creating a auto-release mechanism for a structure body, can be very helpful in such a case. By extending "BaseLock" class, this can be easily implemented as below:

C++
// BaseLock.h
 
Class BaseLock
{
public:
...
   class BaseLockObj
   {
   public:
      BaseLockObj(BaseLock *lock);
      virtual ~BaseLockObj();
      BaseLockObj &operator=(const BaseLockObj & b)
      {
         return *this;
      }
   private:
      BaseLockObj();
      BaseLockObj(const BaseLockObj & b){assert(0);m_lock=NULL;}
      /// The pointer to the lock used.
      BaseLock *m_lock;
   }; 
   ... 
};  
/// type definition  for lock object 
typedef BaseLock::BaseLockObj LockObj; 
  • By overriding copy-operator, and making copy-constructor as private, this blocks from copying the object from other.
  • By making default constructor as private, it must explicitly create only by calling constructor with a pointer to BaseLock object.
  • Declare a global type definition for BaseLock::BaseLockObj as LockObj, so it can be easily accessed.

Implementation

C++
// BaseLock.cpp 
 
BaseLock::BaseLockObj::BaseLockObj(BaseLock *lock)
{
   assert(lock); 
   m_lock=lock;
   if(m_lock) 
      m_lock->Lock();
} 
 
BaseLock::BaseLockObj::~BaseLockObj()
{
   if(m_lock)
   {
      m_lock->Unlock();
   }
} 
 
BaseLock::BaseLockObj::BaseLockObj()
{ 
   m_lock=NULL;
} 
  • So by calling "Lock" within the constructor, it is automatically locked, when BaseLockObj is created.
  • Since "UnLock" is called within the destructor, it is automatically unlocked, when BaseLockObj is destroyed.
This can be used across the synchronization primitives including NoLock, since now they share same interfaces and a subclass of BaseLock class.

So use-case example will be as below:

C++
...
BaseLock *someLock = new CriticalSectionEx(); // declared in somewhere accessible
...
void SomeThreadFunc()
{ 
   ...
   if (test==0)
   {
      LockObj lock(someLock);
      // Lock is obtained automatically
      ... 
   } /// When leaving the if statement lock object is destroyed, so the Lock is automatically released
   ...    
} 
... 
  • As above example, create LockObj object as a local variable (in this case, lock) within if statement with the someLock pointer, which points to CriticalSectionEx object, then the critical section is automatically entered.
  • Since we implemented "Unlock" within the destroctor of LockObj, and as C++ mechanism, when leaving the if statement, the local variable "lock" will be automatically deleted, and will leave critical section automatically.

Use-case Examples

Declaring Synchronization Object

C++
...
// MUTEX
BaseLock * pLock = new Mutex ();
// OR
Mutex cLock;
...
// SEMAPHORE
BaseLock * pLock = new Semaphore();
// OR
Semaphore cLock;
...
// CRITICAL SECTION
BaseLock * pLock = new CriticalSectionEx();
// OR
CriticalSectionEx cLock;
...
// NOLOCK
BaseLock *pLock = new NoLock();
// OR
NoLock cLock;
...

Declaring process can be like below, in case of building same application for different environment such as single-threaded, multi-threaded, or multi-process.

C++
... 
BaseLock *pLock=NULL; 
#if SINGLE_THREADED
pLock = new NoLock();
#elif MULTI_THREADED
pLock = new CriticalSectionEx();
#else // MULTI_PROCESS
pLock = new Mutex();
#endif
...   

Declaring Semaphore Object with initial lock count

C++
...
BaseLock * pLock = new Semaphore(2);
// OR
Semaphore cLock(2);
...   

Declaring Mutex/Semaphore Object for using across process

C++
... 
BaseLock * pLock = new Mutex (_T("MutexName")); 
//OR  
Mutex cLock(_T("MutexName"));
...
 
// For semaphore, lock count must be input, in order to give semaphore name 
BaseLock * pLock = new Semaphore(1, _T("SemaphoreName")); 
//OR 
Semaphore cLock(1, _T("SemaphoreName"));  
... 
  • Note that for Semaphore, initial lock count must be entered in order to give Semaphore's name.

Synchronization Usage Type1 (Manual Lock)

C++
void SomeFunc(SomeClass *sClass)
{ 
   ... 
   pLock->Lock();   //OR cLock.Lock();
   ... 
   pLock->Unlock(); //OR cLock.Unlock();
} 
  • This is straight-forward lock and unlock usage in general.

Synchronization Usage Type2 (Whole Structure Body Lock)

This is a use-case example, where it uses the auto-release lock mechanism (LockObj).

C++
void SomeFunc()
{ 
   LockObj lock(pLock); //OR LockObj lock(&cLock);
   ...
} // Lock is auto-released when exiting the function. 

Or Something like:

C++
void SomeFunc(bool doSomething)
{ 
   ... 
   if(doSomething)
   { 
      LockObj lock(pLock); //OR LockObj lock(&cLock);
      ...
   }// Lock is auto-released when exiting the if-statement.
   ...
}  

Synchronization Usage Type3 (TryLock/TryLockFor)

C++
... 
if(pLock->TryLock()) //OR cLock.TryLock()
{
   // Lock obtained
   ...
   pLock->Unlock(); //OR cLock.Unlock();
}  
... 

Or for TryLockFor (TryLock for certain period of time)

C++
...
if(pLock->TryLockFor(100)) //OR cLock.TryLockFor(100) // TryLock for 100 millisecond
{
   // Lock obtained
   ...
   pLock->Unlock(); //OR cLock.Unlock();
}   

More Practical Example Sources

More practical examples can be found from EpServerEngine Sources.

(Please, see "EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock" article for more detail.)  

Conclusion

As I said in Introduction, there are many advanced Lock Framework library such as boost library, or Intel TBB. However, in many cases, they might be little over-kill to link with your project. This is a very simple guide to show how you can build a Lock Framework of your own. You can change or expand the functionality as you like according to your needs or taste. Hope this helps to your painful synchronization development.

Reference

History

  • 08.22.2013: - Re-distributed under MIT License  
  • 11.16.2012: - Minor updates with suggestion, and source updated  
  • 09.21.2012: - Table of Contents updated 
  • 08.10.2012: - Added new sections
  • "Background"
  • "Critical Section vs Mutex vs Semaphore"
  • 08.04.2012: - Moved the article's subsection to "Howto"
  • 08.03.2012: - Submitted the article.

License

This article, along with any associated source code and files, is licensed under The MIT License