Improving how you use CSingleLock
This posting covers a brief background:
- Win32 critical sections.
- How CCriticalSection and CSingleLock can be used instead of Win32 critical sections.
- An improved way to use CSingleLock.
- Some ways CSingleLock can be used that do not have the desired effect.
Critical Sections in Win32
The Win32 API uses InitializeCriticalSection, EnterCriticalSection, LeaveCriticalSection and DeleteCriticalSection to manage critical sections (CRITICAL_SECTION). Using these APIs is not particularly hard, but nonetheless, it is possible to use critical sections that have not been initialized or that have been deleted. It is also possible to forget to leave a critical section that has been entered. In addition, any exceptions that get thrown may result in a critical section being left in its locked state.
This can cause serious performance problems as locks are held for too long, or in the case of a lock not being released, it can prevent other threads from gaining access to the resource the lock was protecting, possibly resulting in a deadlock.
Example Win32 usage (assume critical section initialized in a different function):
void someFunc() { doWork(); EnterCriticalSection(&cs); doWorkEx(); LeaveCriticalSection(&cs); }
Why use CSingleLock and CMultiLock?
When using critical sections in MFC you use the CCriticalSection class instead of CRITICAL_SECTION objects.
You can directly call Lock() and Unlock() on the CCriticalSection, but it is recommended that you use CSingleLock and CMultiLock to manage your CCriticalSection objects.
The benefits of using a class such as CSingleLock (and its related class CMultiLock) are that:
- The CSingleLock manages the activities of entering and leaving the critical section – you do not have to think about the critical section at all.
- Any CCriticalSection object used with CSingleLocks will automatically be initialized before the CSingleLock gets to work with it.
- The CSingleLock is automatically unlocked (if it was locked) when the CSingleLock is deleted and thus the CCriticalSection that was associated with this CSingleLock is not held locked.
- If an exception is thrown, C++ objects are cleaned up by the exception handling chain, thus automatically deleting any CSingleLock objects and releasing any locks they hold.
- CSingleLock can be used to lock and unlock critical sections just like the old Win32 methods, allowing for easy conversion of code from Win32 style to CSingleLock style.
- It is possible to create a CSingleLock that is automatically locked. This is very useful for set-and-forget critical section management. Just put the CSingleLock in the right place and you can ignore it in the rest of the code. Very neat, convenient and elegant.
One way of using CSingleLock
As described above a typical style of using CSingleLocks echoes the Win32 style of using critical sections.
void someFunc() { CSingleLock lock(&csSect); doWork(); lock.Lock(); doWorkEx(); lock.Unlock(); }
As you can see, the CSingleLock lock manager is created, the doWork() function is called outside of the protected area, the lock is locked, doWorkEx() is called, and then the lock is unlocked. This is a very similar style of writing to the Win32 equivalent.
A better way of using CSingleLock
The problem with the previous way of using CSingleLock is that most of the power and convenience of CSingleLock is ignored. Lock management has been made explicit via calls to Lock() and Unlock(). This means there is potential for forgetting to lock the CSingleLock, or for unlocking the CSingleLock later than desirable.
An improved way of using CSingleLock is to always create CSingleLocks in the locked state and to create CSingleLocks as close to the resource as they are needed to protect.
The following example shows the same function written using a CSingleLock that is automatically locked, created just before it is required and automatically destroyed at the end of the function.
void someFunc() { doWork(); CSingleLock lock(&csSect, TRUE); doWorkEx(); }
If I wanted to do some more work after doWorkEx() but I didn’t want that protected by the lock I could do it by using C++’s scoping capabilities. I simply create a new scope and place the CSingleLock in there. At the end of the scope the CSingleLock is destroyed and the lock is unlocked.
void someFunc() { doWork(); { CSingleLock lock(&csSect, TRUE); doWorkEx(); } doMoreWork(); }
Some problems we have seen…
During the development of code for the software tools at Software Verification and our private tools, we’ve found a few interesting mistakes. Mistakes are often made not through poor design, but simply a typing oversight or mistake, possibly due to the tiredness of the person working on the code – the type of mistake you can only put down to the fact that humans do make mistakes, no matter how talented they are in any given field.
Where possible we like to use the CSingleLock lock(&csSect, TRUE) automatic locking style coupled with tight scoping to make the lock lifetime short. As a result, we are interested in finding the following coding constructs which will result in errors in expected behaviour in our software:
- CSingleLock created without a lock argument. This defaults to an unlocked CSingleLock.
CSingleLock lock(&csSect);
- CSingleLock created with a FALSE lock argument. This is an unlocked CSingleLock.
CSingleLock lock(&csSect, FALSE);
- CSingleLock created without a variable declaration. This compiles but creates a lock that is immediately destroyed. Any of these three variants are interesting as none of the are useful, but all compile OK.
CSingleLock(&csSect);
CSingleLock(&csSect, FALSE);
CSingleLock(&csSect, TRUE);
Wouldn’t it be useful if you could automatically detect these errors?
Thread Lock Checker
The problem with the examples we show above is that looking for them is hard work because humans often read what they expect to read (this is part of our predictive pattern recognition built into how we process shapes and text). As a result, you may be looking right an error and not see it, but you may see the error the next time you come to the code (having forgotten all about it).
To aid in the discovery of these types of lock usage (for both CSingleLock, CMultiLock and any named classes that have the same style of behaviour) we have written a software tool, Thread Lock Checker.
We use Thread Lock Checker before we release any software. We use Thread Lock Checker to scan our codebase looking for any mistakes not identified by our software engineers.
It’s a very useful tool.
We hope that you will also find Thread Lock Checker useful.