Concepts
Win32 provides several different methods of managing threads and the way that they
access shared resources, such as a static or global variable. These include critical sections,
mutexes, semaphores, and events. Of these, critical sections are the simplest and fastest, so
I chose them to make my library thread safe.
An application uses a critical section by first declaring a CRITICAL_SECTION variable
that can then be locked and unlocked as needed to prevent multiple threads from executing in
a certain section of code. When a thread needs to operate on a shared resource, the code
dealing with that resource is bounded by calls to EnterCriticalSection and
LeaveCriticalSection.
// This is a globally-accessible critical section variable
CRITICAL_SECTION cs;
int main()
{
// All critical sections must be initialized prior to use
::InitializeCriticalSection(&cs);
// Program may create and start a few threads here.
// Critical sections are deleted before program termination
::DeleteCriticalSection(&cs);
return 0;
}
DWORD WINAPI ThisIsAThread(LPVOID p)
{
// Do some generic independent processing until
// a shared resource needs to be manipulated
::EnterCriticalSection(&cs);
// Only one thread at a time may execute the code between the
// calls to enter and leave. Here a shared resource may be
// safely manipulated.
::LeaveCriticalSection(&cs);
// Continue independent processing, then exit
return 0;
}
This is a simple example, but in a real application the housekeeping chores can add up.
Since I'm using C++ for my library, I can take advantage of a very useful idiom I first
saw described in The C++ Programming Language.
Resource Acquisition Is Initialization
The resource acquisition is initialization idiom describes a way to let the
semantics of the language take care of the details of acquiring and releasing resources
such as critical sections or other objects that must pair an acquisition with a release.
Look again at the example above. What happens when an exception is thrown after the call
to EnterCriticalSection but before LeaveCriticalSection? What if
a critical section is created (or worse, dozens or hundreds are created) without being
deleted? Whatever you can imagine, it's that bad or worse.
The RAII idiom delgates acquisition and release to a class that performs acquisition when
it is instantiated and performs release when destroyed. In other words, the class constructor
gains access to a resource (such as a lock on a critical section). When the object is destroyed,
(when it goes out of scope, for example) the destructor releases the resource that was acquired in
the constructor.
The CriticalSection Class
Here is the CriticalSection class that I use in my library to manage the lifetime
of a CRITICAL_SECTION variable.
class CriticalSection
{
public:
CriticalSection()
{
::InitializeCriticalSection(&cs);
}
~CriticalSection()
{
::DeleteCriticalSection(&cs);
}
void Enter()
{
::EnterCriticalSection(&cs);
}
void Leave()
{
::LeaveCriticalSection(&cs);
}
private:
CRITICAL_SECTION cs;
};
The class contains a CRITICAL_SECTION that is initialized in the constructor and
deleted in the constructor. The Enter and Leave methods may be
called on an instance of this class to control thread access based on the
critical section object it contains. Here's how it would be used to simplify the
first example:
// Replace CRITICAL_SECTION with an instance of CriticalSection
CriticalSection csObject;
int main()
{
// Program may create and start a few threads here.
return 0;
}
DWORD WINAPI ThisIsAThread(LPVOID p)
{
// Do some generic independent processing until
// a shared resource needs to be manipulated
csObject.Enter();
// Only one thread at a time may execute the code between the
// calls to enter and leave. Here a shared resource may be
// safely manipulated.
csObject.Leave();
// Continue independent processing, then exit
return 0;
}
This example looks like I forgot to initialize and delete the critical section, but
it happens automatically because the csObject constructor executes before
the program enters main, and the destructor executes after main
is finished. This is great, but we still have to deal with the problem of abnormal
thread termination while a critical section is locked.
The Lock Class
The application of RAII to the initialization and deletion of the critical section was
so elegant, why don't we apply it to entering and leaving the critical section as well?
class Lock
{
public:
Lock(CriticalSection& cs) : cs(cs)
{
cs.Enter();
}
~Lock()
{
cs.Leave();
}
private:
CriticalSection& cs;
// A developer is not allowed to create a standalone Lock
// object, so this static satisfies the compiler when the
// default constructor is made private.
static CriticalSection dummy;
Lock() : cs(dummy) {}
};
Now it becomes very easy to safely enter and leave a critical section. Just instantiate
a Lock object with a reference to an existing CriticalSection object.
The critical section will be acquired in the constructor, and it will be released in the
destructor when the Lock object goes out of scope, regardless of the manner by
which scope was exited.
// Replace CRITICAL_SECTION with an instance of CriticalSection
CriticalSection csObject;
int main()
{
// Program may create and start a few threads here.
return 0;
}
DWORD WINAPI ThisIsAThread(LPVOID p)
{
// Do some generic independent processing until
// a shared resource needs to be manipulated
// Wrap the thread-safe code in a scope
{
// Creation occurs when execution enters the scope
Lock lock(csObject);
// Shared resource is manipulated.
}
// The lock is released automatically when execution leaves the scope
return 0;
}
As you can see, RAII is a very powerful idiom. It is also commonly used in smart
pointers (such as std::auto_ptr) or other classes that manage resources
(such as BeginPaint in WindowLib). Whenever you find yourself trying to
match resource acquisitions with releases, consider applying this idiom.