Table of contents
- Introduction
- Background
- Critical Section vs
Mutex vs Semaphore
- Critical Section
- Mutex
- Semaphore
- More Info.
- Purpose of the Lock Framework
- BaseLock Class
- Critical Section Class
- Constructor/Copy Constructor
- Destructor
- Lock/TryLock
- TryLockFor
- Unlock
- Semaphore Class
- Constructor/Copy Constructor
- Destructor
- Lock/TryLock/TryLockFor
- Unlock
- Release
- Mutex Class
- Constructor/Copy Constructor
- Destructor
- Lock/TryLock/TryLockFor
- Unlock
- NoLock Class
- Implementation
- Auto-release Lock
- Implementation
- Use-case Examples
- Declaring Synchronization Object
- Declaring Semaphore Object with initial lock count
- Declaring Mutex/Semaphore Object for using across process
- Synchronization Usage Type1 (Manual Lock)
- Synchronization Usage Type2 (Whole Structure Body Lock)
- Synchronization Usage Type3 (TryLock/TryLockFor)
- More Practical Example Sources
- Conclusion
- 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):
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.
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.
#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
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
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
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
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
- Try to enter the critical section using
TryEnterCriticalSection
- If succeeded, return with code from
TryEnterCriticalSection
- 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
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.
#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:
HANDLE m_sem;
LPSECURITY_ATTRIBUTES m_lpsaAttributes;
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
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
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
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
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
".
void Semaphore::Release(long count, long * retPreviousCount)
{
return ReleaseSemaphore(m_sem,count,retPreviousCount);
}
This can be used as below:
BaseLock *lock = new Semaphore(10);
...
BOOL ret = dynamic_cast<Semaphore*>(lock)->Release(5);
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.
#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:
HANDLE m_mutex;
LPSECURITY_ATTRIBUTES m_lpsaAttributes;
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
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
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
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
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.
#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
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:
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;}
BaseLock *m_lock;
};
...
};
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
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:
...
BaseLock *someLock = new CriticalSectionEx(); ...
void SomeThreadFunc()
{
...
if (test==0)
{
LockObj lock(someLock);
...
} ...
}
...
- 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
...
BaseLock * pLock = new Mutex ();
Mutex cLock;
...
BaseLock * pLock = new Semaphore();
Semaphore cLock;
...
BaseLock * pLock = new CriticalSectionEx();
CriticalSectionEx cLock;
...
BaseLock *pLock = new NoLock();
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.
...
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
...
BaseLock * pLock = new Semaphore(2);
Semaphore cLock(2);
...
Declaring Mutex/Semaphore Object for using across process
...
BaseLock * pLock = new Mutex (_T("MutexName"));
Mutex cLock(_T("MutexName"));
...
BaseLock * pLock = new Semaphore(1, _T("SemaphoreName"));
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)
void SomeFunc(SomeClass *sClass)
{
...
pLock->Lock(); ...
pLock->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
).
void SomeFunc()
{
LockObj lock(pLock); ...
}
Or Something like:
void SomeFunc(bool doSomething)
{
...
if(doSomething)
{
LockObj lock(pLock); ...
} ...
}
Synchronization Usage Type3 (TryLock/TryLockFor)
...
if(pLock->TryLock()) {
...
pLock->Unlock(); }
...
Or for TryLockFor (TryLock for certain period of time)
...
if(pLock->TryLockFor(100)) {
...
pLock->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.