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 }