When writing multithreaded C++ programs on WIN32 platforms, you need to protect certain shared objects so that
only one thread can access them at any given time. You can use 5 system functions to achieve this.
They are InitializeCriticalSection, EnterCriticalSection, TryEnterCriticalSection, LeaveCriticalSection, and DeleteCriticalSection
.
In general, these functions are fairly easy to use. You need to declare a CRITICAL_SECTION
variable for a shared object in your program and call the
InitializeCriticalSection
function to initialize it. Next, call EnterCriticalSection
before accessing the shared object and call LeaveCriticalSection
when you are done with the object. The DeleteCriticalSection
function is called to release all system resources associated with the critical section. Of course, you can also use the critical section classes
in MFC and ATL.
I am wondering why do we need "system" resources to implement critical section? What types of "system" resources are we talking about?
Unfortunately, even after searching MSDN, I still did not find the answers. Since I am used to doing things my own way, I came up with my own implementation, which may
have some advantages. For example, there is no need to initialize or release any system resource. My implementation works on Windows
98 or later and Windows NT 4.0 or later (on the other hand, the TryEnterCriticalSection
system function is not supported on Windows 98).
After some testing, I found that my implementation seems to be (or should I say, probably? maybe?) more efficient than using the system functions, please see my comment
'A very interesting comparison' listed after the article.
Here is my XYCriticalSection class. It does not depend on any library (MFC, ATL, STL, etc.). However, the idea is not exactly new.
#ifndef XYCRITICALSECTION_H
#define XYCRITICALSECTION_H
#include "windows.h"
class XYCriticalSection
{
long m_nLockCount;
long m_nThreadId;
bool SetLock(constlong nThreadId);
public:
XYCriticalSection()
{
m_nThreadId = 0;
m_nLockCount = 0;
}
void Enter();
void Leave();
bool Try();
};
#endif
As you can see, there are only 3 public methods in the XYCriticalSection
class - Enter
, Leave
, and Try
.
The Enter
method will block as long as some other thread is in the critical section. The Try
method is non-blocking.
It returns true
if the critical section is entered successfully, otherwise it returns false
. The Leave
method must be called
once for each call of Enter
and each successful call of Try
.
To use this class in your program, just declare an instance of XYCriticalSection
for each shared object you want to protect.
Call Enter
before accessing the object and call Leave
after you are done with it. There is no system resource to initialize
or release. Please note that the same thread can enter a critical section multiple times without blocking itself.
One inconvenience with this implementation is that you have to make sure that your thread calls the Leave
method after entering
the critical section, otherwise the critical section will be locked and no other thread can enter. What can happen is, even if the calls to Enter
and Leave
are paired in your code, some unexpected exception will cause the thread to skip the Leave
method causing a shared object to be locked forever.
I wrote another class, XYLock
, to help with this situation. All you have to do is declare an XYLock
variable using a pointer to the
XYCriticalSection
instance associated with the
shared object. The constructor of XYLock
will call XYCriticalSection::Enter
and
the destructor will call XYCriticalSection::Leave
. So there is no way you will forget to call Leave!. Here is the XYLock
class.
#ifndef XYLOCK_H
#define XYLOCK_H
#include "XYCriticalSection.h"
class XYLock
{
XYCriticalSection* m_pCS;
public:
XYLock(XYCriticalSection* pCS)
{
m_pCS = pCS;
if(m_pCS)
m_pCS->Enter();
}
~XYLock()
{
if(m_pCS)
m_pCS->Leave();
}
};
#endif
When an XYLock
variable goes out of scope, the lock on the corresponding critical section will be released automatically.
Let's see a simple example of defining a thread-safe C++ object using an XYLock
object.
#ifndef SAFEOBJ_H
#define SAFEOBJ_H
#include "XYLock.h"
class SafeObj
{
XYCriticalSection m_cs;
public:
void SafeMethod()
{
XYLock myLock(&m_cs);
}
};
The code in the public member function SafeMethod
following the declaration of the myLock
variable can only be executed by one thread at any given time.
Please note that an XYLock
variable should only be declared on the stack (at the beginning of the code block that accesses the shared object).
Here is a multithreaded C++ program that further demonstrates the use of XYLock
.
#include <stdio.h>
#include
#include "XYLock.h"
XYCriticalSection myCriticalSection;
void ThreadProc(void *dummy)
{
while(true)
{
try
{
XYLock myLock(&myCriticalSection);
printf("Thread '%X' has the lock\n",::GetCurrentThreadId());
throw "a test";
}
catch(char* p)
{
printf("Caught: %s\n",p);
}
::Sleep(1000);
}
}
void main()
{
for(int i=0;i<100;i++)
{
if(_beginthread(ThreadProc,0,NULL)==-1)
{
printf("Failed to create a thread\n");
return;
}
}
while(true)
{
try
{
XYLock myLock(&myCriticalSection);
printf("The main thread '%X' has the lock\n",::GetCurrentThreadId());
throw "a test from the main thread";
}
catch(char* p)
{
printf("Caught: %s\n",p);
}
::Sleep(1000);
}
}
The above code declares a global XYCriticalSection
variable to be associated with a shared object which you want to protect
from simultaneous accesses by multiple threads. The main function will create 100 threads that forever enter and leave the same critical section.
The id of the current thread that owns the critical section is printed to the console window.
Thanks for reading my articles.