RWMutex: Shared/proprietary recursive mutex

Keywords: C++

Mutual Exclusive Locks with Shared/Exclusive Access Rights and Upgrade/Downgrade Functions

 

introduce

My goal is to create objects that can act as read/write locking mechanisms. Any thread can lock it for reading, but only one thread can lock it for writing. All other threads will wait until the write thread releases it. Writing threads do not acquire mutexes until any other threads are released.

 

I can use Slim Reader/Writer locks, but:

  • They are not recursive, for example, AcquireSRWLockExclusive() if the same thread calls the same function earlier, the call to the pair will block.
  • They are not upgradable, for example, threads that have locked locks to read access rights cannot lock locks to write operations.
  • They are not replicable handles.

I can try C++ 14, shared_lock, but I still need C++ 11 support. In addition, I am not sure if it can really meet my requirements.

So I had to implement it manually. Due to the lack, the common C++ 11 method WaitForMultipleObjects (nyi) was deleted. Now it has upgrade/downgrade function.

 

RWMUTEX

This section is very simple.

1 class RWMUTEX
2     {
3     private:
4         HANDLE hChangeMap;
5         std::map<DWORD, HANDLE> Threads;
6         RWMUTEX(const RWMUTEX&) = delete;
7         RWMUTEX(RWMUTEX&&) = delete;

 

I need std:: map < DWORD, HANDLE > to store a handle for all threads trying to access shared resources, and a mutex lock to ensure that all changes to this mapping are thread-safe.

Constructor

1 RWMUTEX(const RWMUTEX&) = delete;
2 void operator =(const RWMUTEX&) = delete;
3 
4 RWMUTEX()
5     {
6     hChangeMapWrite = CreateMutex(0,0,0);
7     }

 

Simply create a handle that maps mutually exclusive. Objects cannot be copied.

Establish

 1 HANDLE CreateIf(bool KeepReaderLocked = false)
 2     {
 3                 WaitForSingleObject(hChangeMap, INFINITE);
 4                 DWORD id = GetCurrentThreadId();
 5                 if (Threads[id] == 0)
 6                     {
 7                     HANDLE e0 = CreateMutex(0, 0, 0);
 8                     Threads[id] = e0;
 9                     }
10                 HANDLE e = Threads[id];
11                 if (!KeepReaderLocked)
12                       ReleaseMutex(hChangeMap);
13                 return e; 
14     }

 

This private function is called when LockRead() or LockWrite() is called to lock the object. This function creates a mutex for the thread if the current thread has not changed itself into a thread that may access the mutex. If other threads have locked the mutex for write access, the function will block until the write thread releases the object. This function returns the mutex handle of the current thread.

Lock Read/Release Read

 1 HANDLE LockRead()
 2     {
 3     auto f = CreateIf();
 4     WaitForSingleObject(f,INFINITE);
 5     return f;
 6     }
 7 void ReleaseRead(HANDLE f)
 8     {
 9     ReleaseMutex(f);
10     }

 

These functions are called when you want to lock the object for read access and release it later.

Lock / release

 1 void LockWrite()
 2     {
 3                 CreateIf(true);
 4 
 5                 // Wait for all 
 6                 vector<HANDLE> AllThreads;
 7                 AllThreads.reserve(Threads.size());
 8                 for (auto& a : Threads)
 9                     {
10                     AllThreads.push_back(a.second);
11                     }
12 
13                 WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, INFINITE);
14 
15                 // Reader is locked
16     }
17 
18 void ReleaseWrite()
19     {
20     
21     // Release All
22     for (auto& a : Threads)
23         ReleaseMutex(a.second);
24     ReleaseMutex(hChangeMap);
25     }

 

These functions are called when you want to lock the object for write access and release it later. The function functions as follows:

1. No new threads were registered during the lock

2. Any read thread releases the lock

 

Destructor

1 RWMUTEX()
2     {
3     CloseHandle(hChangeMap);
4     hChangeMap = 0;
5     for (auto& a : Threads)
6         CloseHandle(a.second);
7     Threads.clear();
8     }

 

The destructor ensures that all handles are cleared.

Upgradable/Upgradable Locks

Sometimes, you want to upgrade a read lock to a write lock instead of unlocking it first to improve efficiency. Therefore, LockWrite was modified to:

 1 void LockWrite(DWORD updThread = 0)
 2 {
 3     CreateIf(true);
 4 
 5     // Wait for all
 6     AllThreads.reserve(Threads.size());
 7     AllThreads.clear();
 8     for (auto& a : Threads)
 9     {
10         if (updThread == a.first) // except ourself if in upgrade operation
11             continue;
12         AllThreads.push_back(a.second);
13     }
14     auto tim = WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, wi);
15 
16     if (tim == WAIT_TIMEOUT && wi != INFINITE)
17         OutputDebugString(L"LockWrite debug timeout!");
18 
19     // We don't want to keep threads, the hChangeMap is enough
20     // We also release the handle to the upgraded thread, if any
21     for (auto& a : Threads)
22         ReleaseMutex(a.second);
23 
24     // Reader is locked
25 }
26 
27 void Upgrade()
28 {
29     LockWrite(GetCurrentThreadId());
30 }
31 
32 HANDLE Downgrade()
33 {
34     DWORD id = GetCurrentThreadId();
35     auto z = Threads[id];
36     auto tim = WaitForSingleObject(z, wi);
37     if (tim == WAIT_TIMEOUT && wi != INFINITE)
38         OutputDebugString(L"Downgrade debug timeout!");
39     ReleaseMutex(hChangeMap);
40     return z;
41 }

 

Calling Upgrade() now results in:

Change the locked mapping

Waiting for all read threads except our own to exit

Then we release our own thread mutex, because changing the mapping of the lock is enough.

Call Downgrade() result:

  • Get the handle directly from the mapping without re-locking
  • Lock this handle as if we were in read mode
  • Publish Change Mapping

So the whole code is (with some debugging help):

  1 // RWMUTEX
  2     class RWMUTEX
  3         {
  4         private:
  5             HANDLE hChangeMap = 0;
  6             std::map<DWORD, HANDLE> Threads;
  7             DWORD wi = INFINITE;
  8             RWMUTEX(const RWMUTEX&) = delete;
  9             RWMUTEX(RWMUTEX&&) = delete;
 10             operator=(const RWMUTEX&) = delete;
 11 
 12         public:
 13 
 14             RWMUTEX(bool D = false)
 15                 {
 16                 if (D)
 17                     wi = 10000;
 18                 else
 19                     wi = INFINITE;
 20                 hChangeMap = CreateMutex(0, 0, 0);
 21                 }
 22 
 23             ~RWMUTEX()
 24                 {
 25                 CloseHandle(hChangeMap);
 26                 hChangeMap = 0;
 27                 for (auto& a : Threads)
 28                     CloseHandle(a.second);
 29                 Threads.clear();
 30                 }
 31 
 32             HANDLE CreateIf(bool KeepReaderLocked = false)
 33                 {
 34                 auto tim = WaitForSingleObject(hChangeMap, INFINITE);
 35                 if (tim == WAIT_TIMEOUT && wi != INFINITE)
 36                     OutputDebugString(L"LockRead debug timeout!");
 37                 DWORD id = GetCurrentThreadId();
 38                 if (Threads[id] == 0)
 39                     {
 40                     HANDLE e0 = CreateMutex(0, 0, 0);
 41                     Threads[id] = e0;
 42                     }
 43                 HANDLE e = Threads[id];
 44                 if (!KeepReaderLocked)    
 45                     ReleaseMutex(hChangeMap);
 46                 return e;
 47                 }
 48 
 49             HANDLE LockRead()
 50                 {
 51                 auto z = CreateIf();
 52                 auto tim = WaitForSingleObject(z, wi);
 53                 if (tim == WAIT_TIMEOUT && wi != INFINITE)
 54                     OutputDebugString(L"LockRead debug timeout!");
 55                 return z;
 56                 }
 57 
 58     void LockWrite(DWORD updThread = 0)
 59     {
 60         CreateIf(true);
 61 
 62         // Wait for all 
 63         AllThreads.reserve(Threads.size());
 64         AllThreads.clear();
 65         for (auto& a : Threads)
 66         {
 67             if (updThread == a.first) // except ourself if in upgrade operation
 68                 continue;
 69             AllThreads.push_back(a.second);
 70         }
 71         auto tim = WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, wi);
 72 
 73         if (tim == WAIT_TIMEOUT && wi != INFINITE)
 74             OutputDebugString(L"LockWrite debug timeout!");
 75 
 76         // We don't want to keep threads, the hChangeMap is enough
 77         // We also release the handle to the upgraded thread, if any
 78         for (auto& a : Threads)
 79             ReleaseMutex(a.second);
 80 
 81         // Reader is locked
 82     }
 83 
 84     void ReleaseWrite()
 85     {
 86         ReleaseMutex(hChangeMap);
 87     }
 88 
 89     void ReleaseRead(HANDLE f)
 90     {
 91         ReleaseMutex(f);
 92     }
 93 
 94     void Upgrade()
 95     {
 96         LockWrite(GetCurrentThreadId());
 97     }
 98 
 99     HANDLE Downgrade()
100     {
101         DWORD id = GetCurrentThreadId();
102         auto z = Threads[id];
103         auto tim = WaitForSingleObject(z, wi);
104         if (tim == WAIT_TIMEOUT && wi != INFINITE)
105             OutputDebugString(L"Downgrade debug timeout!");
106         ReleaseMutex(hChangeMap);
107         return z;
108     }              
109 };

 

To use RWMUTEX, you can simply create locking classes:

 1 class RWMUTEXLOCKREAD
 2     {
 3     private:
 4         RWMUTEX* mm = 0;
 5     public:
 6 
 7         RWMUTEXLOCKREAD(const RWMUTEXLOCKREAD&) = delete;
 8         void operator =(const RWMUTEXLOCKREAD&) = delete;
 9 
10         RWMUTEXLOCKREAD(RWMUTEX*m)
11             {
12             if (m)
13                 {
14                 mm = m;
15                 mm->LockRead();
16                 }
17             }
18         ~RWMUTEXLOCKREAD()
19             {
20             if (mm)
21                 {
22                 mm->ReleaseRead();
23                 mm = 0;
24                 }
25             }
26     };
27 
28 class RWMUTEXLOCKWRITE
29     {
30     private:
31         RWMUTEX* mm = 0;
32     public:
33         RWMUTEXLOCKWRITE(RWMUTEX*m)
34             {
35             if (m)
36                 {
37                 mm = m;
38                 mm->LockWrite();
39                 }
40             }
41         ~RWMUTEXLOCKWRITE()
42             {
43             if (mm)
44                 {
45                 mm->ReleaseWrite();
46                 mm = 0;
47                 }
48             }
49     };

 

There is also a new class for upgrading mechanisms:

 1 class RWMUTEXLOCKREADWRITE
 2 {
 3 private:
 4     RWMUTEX* mm = 0;
 5     HANDLE lm = 0;
 6     bool U = false;
 7 public:
 8 
 9     RWMUTEXLOCKREADWRITE(const RWMUTEXLOCKREADWRITE&) = delete;
10     void operator =(const RWMUTEXLOCKREADWRITE&) = delete;
11 
12     RWMUTEXLOCKREADWRITE(RWMUTEX*m)
13     {
14         if (m)
15         {
16             mm = m;
17             lm = mm->LockRead();
18         }
19     }
20 
21     void Upgrade()
22     {
23         if (mm && !U)
24         {
25             mm->Upgrade();
26             lm = 0;
27             U = 1;
28         }
29     }
30 
31     void Downgrade()
32     {
33         if (mm && U)
34         {
35             lm = mm->Downgrade();
36             U = 0;
37         }
38     }
39 
40     ~RWMUTEXLOCKREADWRITE()
41     {
42         if (mm)
43         {
44             if (U)
45                 mm->ReleaseWrite();
46             else
47                 mm->ReleaseRead(lm);
48             lm = 0;
49             mm = 0;
50         }
51     }
52 };

 

Examples of usage:

 1 RWMUTEX m;
 2 
 3 // ... other code
 4 void foo1() {
 5   RWMUTEXLOCKREAD lock(&m);
 6   }
 7 
 8 void foo2() {
 9  RWMUTEXLOCKWRITE lock(&m);
10 }

 

Posted by macinslaw on Sat, 12 Oct 2019 00:45:28 -0700