Search code examples
c++multithreadingmutexdeadlockcondition-variable

Reader doesn't wake up once writer finishes writting in Reader writer lock


class ReadLock
{
private:
    std::mutex readWriteMutex;
    std::mutex conditionmtx;
    std::condition_variable cv;
    int readings = 0;
    int writings = 0;
    int writers = 0;
public:
    void AquireReadLock()
    {
        readWriteMutex.lock();
        if (writers)
        {
            std::unique_lock<std::mutex> lck(conditionmtx);
            cv.wait(lck);
        }
        while (writings)
        {
            std::unique_lock<std::mutex> lck(conditionmtx);
            cv.wait(lck);
        }
        readings++;
        readWriteMutex.unlock();
    }
    void ReleaseReadLock()
    {
        readWriteMutex.lock();
        //std::unique_lock<std::mutex> lck(conditionmtx);
        cv.notify_all();
        readings--;
        readWriteMutex.unlock();
    }
    void  AquireWriteLock()
    {
        readWriteMutex.lock();
        writers++;
        while (readings || writings)
        {
            std::unique_lock<std::mutex> lck(conditionmtx);
            cv.wait(lck);
        }
        writings++;
        readWriteMutex.unlock();
    }
    void ReleaseWriteLock()
    {
        readWriteMutex.lock();
        writings--;
        writers--;;
        //std::unique_lock<std::mutex> lck(conditionmtx);
        cv.notify_all();
        readWriteMutex.unlock();
    }

};

ReadLock lock;
void WriteFunction(int id)
{
    std::cout << "thread " + std::to_string(id) + " asks for write " << '\n';
    lock.AquireWriteLock();
    std::cout << "thread " + std::to_string(id) + " writting" << '\n';
    std::this_thread::sleep_for(std::chrono::milliseconds(3500));
    std::cout << "thread " + std::to_string(id) + " finished writting" << '\n';
    lock.ReleaseWriteLock();
}
void ReadFunction(int id)
{
    if (id == 0)
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "thread " + std::to_string(id) + " asks for read" << '\n';
    lock.AquireReadLock();
    std::cout << "thread " + std::to_string(id) + " reading" << '\n';
    std::this_thread::sleep_for(std::chrono::milliseconds(2500));
    std::cout << "thread " + std::to_string(id) + " finished reading" << '\n';
    lock.ReleaseReadLock();
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::thread threads[3];

    for (int i = 0; i < 3; ++i)
    if (i % 2 == 0)
        threads[i] = std::thread(ReadFunction, i);
    else
        threads[i] = std::thread(WriteFunction, i);

    for (auto& th : threads) th.join();

}

i am trying to implement reader writer lock using condition variable and Mutex. Thread 2 writes first and thread 0 and thread 1 waits for thread 2 to finish writing but once thread 2 finished writing thread 1 and thread 0 are not waking up to read.can someone Help me out with this ? i am new to c++ synchronisation


Solution

  • This code generates a deadlock either on the writerRelease() or the readerRealease() debending which was fist aqcuired.

    How to find out ?

    Multithread code is difficult to debug. I suggest you to add here some logging with to show when entering in an aquire/release function and when the mutex was locked.

    For example:

    void ReleaseReadLock()
    {
        cout <<this_thread::get_id()<< " will release ReadLock" << endl;
        readWriteMutex.lock();
        cout << this_thread::get_id() << " ...mutex locked" << endl;
        cv.notify_all();
        cout << this_thread::get_id() << " ...notified" << endl;
        readings--;
        readWriteMutex.unlock();
        cout << this_thread::get_id()<<" released ReadLock " << endl;
    }
    

    With such code, you will observe this scenario (or a slight variant of it):

    5204 thread 1 asks for write
    3692 thread 2 asks for read
    5204 will aquire WriteLock   ==> start write lock acquisition 
    3692 will aquire ReadLock    ==> start read lock acquisition  
    3692 ...mutex locked         ==> mutex was locked for read lock  acquisition
    3692 aquired ReadLock        ==> mutex was unlocked : end read lock acquisition. 
    5204 ...mutex locked         ==> mutext was locked for writelock 
    3692 thread 2 reading        
    5288 thread 0 asks for read
    5288 will aquire ReadLock    ==> another read lock will wait for mutex
    3692 thread 2 finished reading
    3692 will release ReadLock   ==> Reader can't release lock because mutex is locked by writelock
    

    What happens ?

    The release lock was aquired succesfully. So readings is 1 .

    To decrease this variable, releaseReadLock() has to terminate its job. But it can't because the mutex it needs at the beginning of the function, is still held by the aquireWriteLock(). So it waits.

    But the aquireWriteLock() is stuck in a loop, that will continue to loop as long as readings or writings. It will release the mutex only of readings goes back to 0.

    In short, releaseReadLock() and acquireWriteLock() are both stuck, waiting for each other.

    How to solve it ?

    Well, deadlocks are a pretty nasty thing.

    One thing that helsp is to always perform locking on several objects in the same order. Then one thred might eventiually fail the locking, but there'll be no "kiss of death".

    More specifically, looking at your code, I have the impression that your readWriteMutex is there mostly to protect against race conditions on your 3 counters. I'd suggest to get rid of this mutex and use atomic variables instead.

    Then in ReleaseReadLock() you should decrement the number of readers before notifying the waiting threads. With these two measures, I could run several times without deadlock (which do not prove that it's perfect, but at least the most obvious cases are avoided. up to you to analyse/verify in detail).

    class ReadLock
    {
    private:
        std::mutex conditionmtx;
        std::condition_variable cv;
        atomic<int> readings = 0;   // atomics don't need mutex for being updated
        atomic<int> writings = 0;
        atomic<int> writers = 0;
    public:
        void AquireReadLock()
        {
            cout << this_thread::get_id() << " will aquire ReadLock" << endl;
            if(writers) {
                std::unique_lock<std::mutex> lck(conditionmtx);
                cv.wait(lck);
            }
            while(writings) {
                std::unique_lock<std::mutex> lck(conditionmtx);
                cv.wait(lck);
            }
            readings++;
            cout << this_thread::get_id() << " aquired ReadLock" << endl;
        }
        void ReleaseReadLock()
        {
            cout <<this_thread::get_id()<< " will release ReadLock" << endl;
            readings--;
            cv.notify_all();
            cout << this_thread::get_id()<<" released ReadLock " << endl;
        }
        void  AquireWriteLock()
        {
            cout << this_thread::get_id() << " will aquire WriteLock" << endl;
            writers++;
            while(readings || writings) {
                std::unique_lock<std::mutex> lck(conditionmtx);
                cv.wait(lck);
            }
            writings++;
            cout << this_thread::get_id() << " aquired  WriteLock" << endl;
        }
        void ReleaseWriteLock()
        {
            cout << this_thread::get_id() << " will release WriteLock" << endl;
            writings--;
            writers--;;
            cv.notify_all();
            cout << this_thread::get_id() << " ...notified" << endl;
            cout << this_thread::get_id() << " released WriteLock" << endl;
        }
    };