Introduction
Working with synchronized blocks and data members using Win32 can be a pretty annoying thing. It not only causes bulky code -- especially comparing with languages like Java -- but it also provokes the deadlock bugs. Let's see how we can make our lives simpler in pure C++ without involving additional libraries.
Problem
When you are writing the C++ in pure WinAPI and you have to deal with threads, you'll probably use the synchronization based on CRITICAL_SECTION structure and the family of functions XXXCriticalSection(). Such code usually looks pretty bad even for simple synchronized accessor methods, not to mention dealing with multiple return points in synchronized methods and throwing exceptions within such methods. Don't forget that critical sections should probably be initialized and destroyed. Most likely, you should write something like following:
class A
{
mutable CRITICAL_SECTION m_sync;
int m_some_protected_int_val;
...
A()
{
InitializeCriticalSection(&m_sync);
}
...
~A()
{
DeleteCriticalSection(&m_sync);
}
...
void do_job()
{
EnterCriticalSection(&m_sync);
if()
{
m_some_protected_int_val++;
LeaveCriticalSection(&m_sync);
return;
}
else
{
try
{
throw "exception";
}
catch(char*)
{
LeaveCriticalSection(&m_sync);
throw;
}
}
LeaveCriticalSection(&m_sync);
}
int get() const
{
EnterCriticalSection(&m_sync);
int ret = m_some_protected_int_val;
LeaveCriticalSection(&m_sync);
return ret;
}
};
Do you agree that it is ugly and unforgivably cumbersome? It makes you think about the operator goto, doesn't it? Your mom will be angry!
Solution
It would be really nice if we managed to work with synchronized methods and blocks with the elegance of the languages that were born for it, like Java and C#, and indeed we can! The first step is pretty straightforward. Let's wrap the CRITICAL_SECTION structure into simple a class named critical_section_t that will transparently initialize it in the constructor and delete in the destructor. Now we can write something like this:
class A
{
critical_section_t m_sync;
int m_some_protected_int_val;
....
...
void do_job()
{
m_sync.enter();
if()
{
m_some_protected_int_val++;
m_sync.leave();
return;
}
else
{
try
{
throw "exception";
}
catch(char*)
{
m_sync.leave();
throw;
}
}
m_sync.leave();
}
int get() const
{
m_sync.enter();
int ret = m_some_protected_int_val;
m_sync.leave();
return ret;
}
};
Well, we've gotten rid of initialization and clean-up, but where is the promised Java-like beauty? Patience, it is just one more slight dash to go. Let's define another class named synchronized_block, whose objects will call enter() for us on creation and leave() on destruction. Look how nice it can be now:
class A
{
critical_section_t m_sync;
int m_some_protected_int_val;
...
void do_job()
{
synchronized_block lock(m_sync);
if()
{
m_some_protected_int_val++;
return;
}
else
{
throw "exception";
}
}
int get() const
{
synchronized_block lock(m_sync);
return m_some_protected_int_val;
}
};
From now on, you don't have to be bored by initializing and deleting critical sections and, what is even more important, you don't have to remember to call LeaveCriticalSection() on each return point or block ending. That will finally give you much less doubtful pleasure when dealing with deadlocks. What about throwing exceptions now? Magically, this issue is solved too! Destructors are always called during stack unwinding, so you can forget about the problem. Last but not least: your code looks lots nicer and more clear. Of course, you have already noticed that you can make synchronized blocks exactly like synchronized methods. The method is also a block, isn't it?
Obviously, you can still use the m_sync object itself. For example, if you need to use the TryEnterCriticalSection() API for Windows 2000 and greater only, just call the try_enter() method and leave() afterwards. In addition, the critical_section_t class has a casting operator that allows you to get the address of the underlying structure. Just use (LPCRITICAL_SECTION)m_sync if you really-really need to.
You don't have to worry about performance because both classes are declared fully inline and all of the allocations are done on the stack, so we are still very efficient. No macros, by the way. Did they teach you that macros are evil? My final remark goes to those who are confused by the last get() method. The point is, the destructor of lock is called after the copy constructor of int, so it is perfectly safe. You can freely use the attached code and write me your opinions and questions.
Love C++!
The Files
The attached project was created with MS Visual Studio 2005.
History
- 8 August, 2007 - first version