Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / Win32

RWMutex: A Shared/Exclusive Recursive Mutex

Rate me:
Please Sign up or sign in to vote.
4.98/5 (22 votes)
13 Dec 2018CPOL3 min read 59.5K   30   25
A mutex with shared/exclusive access with upgrade/downgrade capability

Introduction

My aim was to create something that could act as a read/write locking mechanism. Any thread can lock it for reading, but only one thread can lock it for writing. Until the writing thread releases it, all other threads wait. A writing thread does not acquire the mutex until any other thread has released.

I could use Slim Reader/Writer locks, but:

  • They are not recursive, e.g., a call to AcquireSRWLockExclusive() will block if the same thread has called the same function earlier.
  • They are not upgradable, e.g., a thread which has locked the lock for read access can't lock it for write.
  • They are not copyable handles.

I could try C++ 14 shared_lock but I still need C++ 11 support. Besides, I'm not yet sure if it can actually fulfill my requirements.

Therefore, I had to implement it manually. The plain C++ 11 way was removed due to lack of WaitForMultipleObjects (nyi). Now with upgrade/downgrade capabilities.

RWMUTEX

My class is rather simple.

C++
class RWMUTEX
    {
    private:
        HANDLE hChangeMap;
        std::map<DWORD, HANDLE> Threads;
        RWMUTEX(const RWMUTEX&) = delete;
        RWMUTEX(RWMUTEX&&) = delete;

I need a std::map<DWORD,HANDLE> to store handles for all threads that try to access the shared resource, and I also need a mutex handle to make sure that all changes to this map are thread safe.

Constructor

C++
RWMUTEX(const RWMUTEX&) = delete;
void operator =(const RWMUTEX&) = delete;

RWMUTEX()
    {
    hChangeMapWrite = CreateMutex(0,0,0);
    }

Simply create a handle to the changing map mutex. The object should not be copyable.

CreateIf

C++
HANDLE CreateIf(bool KeepReaderLocked = false)
    {
                WaitForSingleObject(hChangeMap, INFINITE);
                DWORD id = GetCurrentThreadId();
                if (Threads[id] == 0)
                    {
                    HANDLE e0 = CreateMutex(0, 0, 0);
                    Threads[id] = e0;
                    }
                HANDLE e = Threads[id];
                if (!KeepReaderLocked)
                      ReleaseMutex(hChangeMap);
                return e; 
    }

This private member function is called when you call LockRead() or LockWrite() to lock the object. If the current thread has not already registered itself to the threads that might access this mutex, this function creates a mutex for that thread. If some other thread has locked this mutex for write access, this function will block until the writing thread releases the object. This function returns the mutex handle for the current thread.

LockRead/ReleaseRead

C++
HANDLE LockRead()
    {
    auto f = CreateIf();
    WaitForSingleObject(f,INFINITE);
    return f;
    }
void ReleaseRead(HANDLE f)
    {
    ReleaseMutex(f);
    }

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

LockWrite/ReleaseWrite

C++
void LockWrite()
    {
                CreateIf(true);

                // Wait for all 
                vector<HANDLE> AllThreads;
                AllThreads.reserve(Threads.size());
                for (auto& a : Threads)
                    {
                    AllThreads.push_back(a.second);
                    }

                WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, INFINITE);

                // Reader is locked
    }

void ReleaseWrite()
    {
    
    // Release All
    for (auto& a : Threads)
        ReleaseMutex(a.second);
    ReleaseMutex(hChangeMap);
    }

These functions are called when you want to lock the object for write access and later release it. LockWrite() function makes sure that:

  1. no new threads are registered during the lock, and
  2. any reading thread has released the lock

Destructor

C++
~RWMUTEX()
    {
    CloseHandle(hChangeMap);
    hChangeMap = 0;
    for (auto& a : Threads)
        CloseHandle(a.second);
    Threads.clear();
    }

The destructor makes sure that all handles are cleared.

Upgradable/Downgradable Locks

Sometimes, you want a read lock to be upgraded to a writing lock, without unlocking first, for efficiency. Therefore, LockWrite is modified as so:

C++
void LockWrite(DWORD updThread = 0)
{
    CreateIf(true);

    // Wait for all
    AllThreads.reserve(Threads.size());
    AllThreads.clear();
    for (auto& a : Threads)
    {
        if (updThread == a.first) // except ourself if in upgrade operation
            continue;
        AllThreads.push_back(a.second);
    }
    auto tim = WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, wi);

    if (tim == WAIT_TIMEOUT && wi != INFINITE)
        OutputDebugString(L"LockWrite debug timeout!");

    // We don't want to keep threads, the hChangeMap is enough
    // We also release the handle to the upgraded thread, if any
    for (auto& a : Threads)
        ReleaseMutex(a.second);

    // Reader is locked
}

void Upgrade()
{
    LockWrite(GetCurrentThreadId());
}

HANDLE Downgrade()
{
    DWORD id = GetCurrentThreadId();
    auto z = Threads[id];
    auto tim = WaitForSingleObject(z, wi);
    if (tim == WAIT_TIMEOUT && wi != INFINITE)
        OutputDebugString(L"Downgrade debug timeout!");
    ReleaseMutex(hChangeMap);
    return z;
}

Calling Upgrade() now results in:

  • Change map is locked
  • Wait for all reading threads to exit except our own

We then release our own threads mutex since locking the change map is enough.

Calling Downgrade() results in:

  • Getting the handle from the map directly, no need to relock
  • Lock this handle as if we are in read mode
  • Release the change map

So the entire code is (with some debugging aid):

C++
// RWMUTEX
    class RWMUTEX
        {
        private:
            HANDLE hChangeMap = 0;
            std::map<DWORD, HANDLE> Threads;
            DWORD wi = INFINITE;
            RWMUTEX(const RWMUTEX&) = delete;
            RWMUTEX(RWMUTEX&&) = delete;
            operator=(const RWMUTEX&) = delete;

        public:

            RWMUTEX(bool D = false)
                {
                if (D)
                    wi = 10000;
                else
                    wi = INFINITE;
                hChangeMap = CreateMutex(0, 0, 0);
                }

            ~RWMUTEX()
                {
                CloseHandle(hChangeMap);
                hChangeMap = 0;
                for (auto& a : Threads)
                    CloseHandle(a.second);
                Threads.clear();
                }

            HANDLE CreateIf(bool KeepReaderLocked = false)
                {
                auto tim = WaitForSingleObject(hChangeMap, INFINITE);
                if (tim == WAIT_TIMEOUT && wi != INFINITE)
                    OutputDebugString(L"LockRead debug timeout!");
                DWORD id = GetCurrentThreadId();
                if (Threads[id] == 0)
                    {
                    HANDLE e0 = CreateMutex(0, 0, 0);
                    Threads[id] = e0;
                    }
                HANDLE e = Threads[id];
                if (!KeepReaderLocked)    
                    ReleaseMutex(hChangeMap);
                return e;
                }

            HANDLE LockRead()
                {
                auto z = CreateIf();
                auto tim = WaitForSingleObject(z, wi);
                if (tim == WAIT_TIMEOUT && wi != INFINITE)
                    OutputDebugString(L"LockRead debug timeout!");
                return z;
                }

    void LockWrite(DWORD updThread = 0)
    {
        CreateIf(true);

        // Wait for all 
        AllThreads.reserve(Threads.size());
        AllThreads.clear();
        for (auto& a : Threads)
        {
            if (updThread == a.first) // except ourself if in upgrade operation
                continue;
            AllThreads.push_back(a.second);
        }
        auto tim = WaitForMultipleObjects((DWORD)AllThreads.size(), AllThreads.data(), TRUE, wi);

        if (tim == WAIT_TIMEOUT && wi != INFINITE)
            OutputDebugString(L"LockWrite debug timeout!");

        // We don't want to keep threads, the hChangeMap is enough
        // We also release the handle to the upgraded thread, if any
        for (auto& a : Threads)
            ReleaseMutex(a.second);

        // Reader is locked
    }

    void ReleaseWrite()
    {
        ReleaseMutex(hChangeMap);
    }

    void ReleaseRead(HANDLE f)
    {
        ReleaseMutex(f);
    }

    void Upgrade()
    {
        LockWrite(GetCurrentThreadId());
    }

    HANDLE Downgrade()
    {
        DWORD id = GetCurrentThreadId();
        auto z = Threads[id];
        auto tim = WaitForSingleObject(z, wi);
        if (tim == WAIT_TIMEOUT && wi != INFINITE)
            OutputDebugString(L"Downgrade debug timeout!");
        ReleaseMutex(hChangeMap);
        return z;
    }              
};

To use the RWMUTEX, you can simply create locking classes:

C++
class RWMUTEXLOCKREAD
    {
    private:
        RWMUTEX* mm = 0;
    public:

        RWMUTEXLOCKREAD(const RWMUTEXLOCKREAD&) = delete;
        void operator =(const RWMUTEXLOCKREAD&) = delete;

        RWMUTEXLOCKREAD(RWMUTEX*m)
            {
            if (m)
                {
                mm = m;
                mm->LockRead();
                }
            }
        ~RWMUTEXLOCKREAD()
            {
            if (mm)
                {
                mm->ReleaseRead();
                mm = 0;
                }
            }
    };

class RWMUTEXLOCKWRITE
    {
    private:
        RWMUTEX* mm = 0;
    public:
        RWMUTEXLOCKWRITE(RWMUTEX*m)
            {
            if (m)
                {
                mm = m;
                mm->LockWrite();
                }
            }
        ~RWMUTEXLOCKWRITE()
            {
            if (mm)
                {
                mm->ReleaseWrite();
                mm = 0;
                }
            }
    };

And a new class for the upgrade mechanism:

C++
class RWMUTEXLOCKREADWRITE
{
private:
    RWMUTEX* mm = 0;
    HANDLE lm = 0;
    bool U = false;
public:

    RWMUTEXLOCKREADWRITE(const RWMUTEXLOCKREADWRITE&) = delete;
    void operator =(const RWMUTEXLOCKREADWRITE&) = delete;

    RWMUTEXLOCKREADWRITE(RWMUTEX*m)
    {
        if (m)
        {
            mm = m;
            lm = mm->LockRead();
        }
    }

    void Upgrade()
    {
        if (mm && !U)
        {
            mm->Upgrade();
            lm = 0;
            U = 1;
        }
    }

    void Downgrade()
    {
        if (mm && U)
        {
            lm = mm->Downgrade();
            U = 0;
        }
    }

    ~RWMUTEXLOCKREADWRITE()
    {
        if (mm)
        {
            if (U)
                mm->ReleaseWrite();
            else
                mm->ReleaseRead(lm);
            lm = 0;
            mm = 0;
        }
    }
};

Sample usage:

C++
RWMUTEX m;

// ... other code
void foo1() {
  RWMUTEXLOCKREAD lock(&m);
  }

void foo2() {
 RWMUTEXLOCKWRITE lock(&m);
}

History

  • 13-12-2018: Added upgrading mechanism
  • 10-12-2017: Added debugging aids and fixed a read/write deadlock by allowing ReleaseRead() not to call CreateIf()
  • 12-05-2017: Fixed rare deadlock in writers, and simplified class
  • 23-08-2016: Fixed deadlock in reader unlocking
  • 17-12-2015: Fixed race bug in reading (and removed C++ 11 implementation)
  • 12-12-2015: First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Greece Greece
I'm working in C++, PHP , Java, Windows, iOS, Android and Web (HTML/Javascript/CSS).

I 've a PhD in Digital Signal Processing and Artificial Intelligence and I specialize in Pro Audio and AI applications.

My home page: https://www.turbo-play.com

Comments and Discussions

 
Questioncan it be compiled in Visual Studio 2017? Pin
Southmountain22-Sep-20 12:10
Southmountain22-Sep-20 12:10 
QuestionFormat question Pin
Nelek10-Jul-19 18:45
protectorNelek10-Jul-19 18:45 
PraiseGreat article Pin
Michael Haephrati15-Dec-18 3:45
professionalMichael Haephrati15-Dec-18 3:45 
QuestionMutex is a waste if it is not cross process synch Pin
steveb30-Nov-18 9:01
mvesteveb30-Nov-18 9:01 
AnswerRe: Mutex is a waste if it is not cross process synch Pin
Michael Chourdakis12-Dec-18 23:12
mvaMichael Chourdakis12-Dec-18 23:12 
QuestionA Question about the Mutex Pin
Rick York4-Apr-17 5:17
mveRick York4-Apr-17 5:17 
AnswerRe: A Question about the Mutex Pin
Rick York4-Apr-17 11:41
mveRick York4-Apr-17 11:41 
GeneralRe: A Question about the Mutex Pin
Michael Chourdakis5-Apr-17 4:44
mvaMichael Chourdakis5-Apr-17 4:44 
GeneralRe: A Question about the Mutex Pin
basementman26-Dec-18 5:37
basementman26-Dec-18 5:37 
GeneralRe: A Question about the Mutex Pin
Michael Chourdakis26-Dec-18 5:41
mvaMichael Chourdakis26-Dec-18 5:41 
QuestionPerformance degradation - you never delete locks from map Pin
Evgeny Zavalkovsky24-May-16 9:00
Evgeny Zavalkovsky24-May-16 9:00 
AnswerRe: Performance degradation - you never delete locks from map Pin
Michael Chourdakis25-May-16 2:18
mvaMichael Chourdakis25-May-16 2:18 
QuestionDeadlock Pin
Evgeny Zavalkovsky24-May-16 8:45
Evgeny Zavalkovsky24-May-16 8:45 
AnswerRe: Deadlock Pin
Michael Chourdakis29-May-16 8:06
mvaMichael Chourdakis29-May-16 8:06 
GeneralRe: Deadlock Pin
Evgeny Zavalkovsky31-May-16 9:57
Evgeny Zavalkovsky31-May-16 9:57 
GeneralRe: Deadlock Pin
Michael Chourdakis22-Aug-16 20:58
mvaMichael Chourdakis22-Aug-16 20:58 
QuestionI don't believe this works Pin
Goran Mitrovic18-Dec-15 10:36
Goran Mitrovic18-Dec-15 10:36 
AnswerRe: I don't believe this works Pin
Michael Chourdakis18-Dec-15 10:42
mvaMichael Chourdakis18-Dec-15 10:42 
GeneralRe: I don't believe this works Pin
Goran Mitrovic18-Dec-15 10:47
Goran Mitrovic18-Dec-15 10:47 
1. While doing LockRead (and accessing the map without the lock) on one thread, LockRead on another can modify the map.
3. Win32 and boost RW locks. I'm sorry if I've overlooked something since I've spent only 5 minutes with your code, but I'm not convinced it works.
- Goran.

GeneralRe: I don't believe this works Pin
Michael Chourdakis18-Dec-15 22:11
mvaMichael Chourdakis18-Dec-15 22:11 
GeneralRe: I don't believe this works Pin
William E. Kempf22-Dec-15 4:12
William E. Kempf22-Dec-15 4:12 
GeneralRe: I don't believe this works Pin
Michael Chourdakis22-Dec-15 4:19
mvaMichael Chourdakis22-Dec-15 4:19 
GeneralRe: I don't believe this works Pin
William E. Kempf22-Dec-15 6:22
William E. Kempf22-Dec-15 6:22 
GeneralRe: I don't believe this works Pin
Michael Chourdakis22-Dec-15 6:25
mvaMichael Chourdakis22-Dec-15 6:25 
GeneralRe: I don't believe this works Pin
William E. Kempf22-Dec-15 7:00
William E. Kempf22-Dec-15 7:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.