Recursive SRW Locks

Windows Vista added new synchronization API, Slim Reader/Writer (SRW) Locks, which is a powerful alternative to critical sections. The detailed description is, as always on MSDN, and what makes it really cool is simple:

  • unlike critical sections, SRW Locks provide reader and writer access synchronization making it possible for 2 and more reader to not block one another
  • SRW Locks do not reference any resources and have size of a pointer, which is the simplest possible scenario; as a result, they don’t need a destructor and their initialization is simple zeroing of memory/variable (for which you however should use InitializeSRWLock API

Being lightweight they are efficient. To understand how at all they can work, one can imagine that a reader might be trying to InterlockedIncrement a synchronization variable. If result is positive, then it’s OK to go. Otherwise, reader should decrement it back, wait and retry. A writer, instead, does InterlockedAdd with an argument of -0x1000 and checks that result of the operation is exactly -0x1000.

This post is about a trap one cat enter into by neglecting one of the SRW lock warnings:

… so SRW locks cannot be acquired recursively. In addition, a thread that owns an SRW lock in shared mode cannot upgrade its ownership of the lock to exclusive mode.

SRW locks cannot be acquired recursively, but it is very easy to make a mistake. If you attempt to recursively acquire, you are likely to succeed, without a warning, error code, exception or assertion failure. You pass this point and you can write quite some code before you realize something is wrong.

It can be as simple as this:

SIZE_T GetCount() const throw()
{
    AcquireSRWLockShared(&m_Lock);
    const SIZE_T nCount = ...;
    ReleaseSRWLockShared(&m_Lock);
    return nCount;
}
BOOL IsEmpty() const throw()
{
    AcquireSRWLockShared(&m_Lock);
    const BOOL bResult = GetCount() > 0;
    ReleaseSRWLockShared(&m_Lock);
    return bResult;
}

IsEmpty calls GetCount which again attempts to acquire lock.

In multit-hreaded environment the problem may come up in the following scenario: thread A acquires shared lock, thread B attempts to acquire exclusive lock, thread A attempts to re-acquire (recursively) shared lock again and both threads freeze at this point. Note that without thread B recursive locking succeeds on thread A.

SrwLockTest01 project provides a sample code to reproduce the deadlock around misused SRW Lock. RecursiveSharedLock and ExclusiveLock thread functions keep acquiring lock simultaneously, while main thread is monitoring current worker thread positions.

Shared access only worker thread, which does recursive lock, runs just fine alone, if the other concurrent thread is commented out:

CHandle SharedLockThread, ExclusiveLockThread;
SharedLockThread.Attach(AtlCreateThread<INT_PTR>(&RecursiveSharedLock, 0));
//ExclusiveLockThread.Attach(AtlCreateThread<INT_PTR>(&ExclusiveLock, 0));
g_nRecursiveSharedLockLine 27, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 29, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 31, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 31, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 29, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 25, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 27, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 25, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 29, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 29, g_nExclusiveLockLine 0
g_nRecursiveSharedLockLine 33, g_nExclusiveLockLine 0

Status output captures worker thread at random line, and CPU is maxed out on one of the cores.

With concurrent threads, a deadlock takes place very soon:

g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49
g_nRecursiveSharedLockLine 35, g_nExclusiveLockLine 49

CPU consumption for the process is nearly zero – threads are in deadlock: shared access thread is i nAPI call at line 36, exclusive access thread is at line 50.

As soon as the problem is clear, a reasonable safety measures are to be taken to avoid misuse. To use SRW Locks safely, it is suggested to use a wrapper class that asserts on misuse in debug builds (to be continued).

Sample code is Visual Studio 2010 C++ project accessible from SVN repository.

See also:

Leave a Reply