1

I'm currently working on a correct implementation of the Reader-Writer problem (see here).

I found this solution in the Qt docks guaranteeing fair treatment of Reader and Writer threads by using a semaphore and mutex. The basic code is this:

sem_t semaphore_;
pthread_mutex_t lock_;

void PalindromeDatabase::initializeLocks()
{
    sem_init(&semaphore_, 0, NumberOfReaders_);
    pthread_mutex_init(&lock_, nullptr);
}

void PalindromeDatabase::lockReaders()
{
    sem_wait(&semaphore_);
}

void PalindromeDatabase::unlockReaders()
{
    sem_post(&semaphore_);
}

void PalindromeDatabase::lockWriters()
{
    pthread_mutex_lock(&lock_);
    {
        for (int i = 0; i < NumberOfReaders_; ++i)
            sem_wait(&semaphore_);
    }
    pthread_mutex_unlock(&lock_);
}

void PalindromeDatabase::unlockWriters()
{
    for (int i = 0; i < NumberOfReaders_; ++i)
        sem_post(&semaphore_);
}

This seems like a very elegant solution. It seems easier and a lot more efficient than the pthread_rwlock_*behavior detailed in this SO answer.

I was wondering if this code below is a correct adjustment of the Qt solution to prefer Reader threads.

int readersActive_;
sem_t semaphore_;
pthread_mutex_t lock_;
pthread_mutex_t readLock_;
pthread_cond_t wait_;

void PalindromeDatabase::initializeLocks()
{
    sem_init(&semaphore_, 0, numberOfReaders_);
    pthread_mutex_init(&lock_, nullptr);
    pthread_mutex_init(&readLock_, nullptr);
    pthread_cond_init(&wait_, nullptr);
}

void PalindromeDatabase::lockReaders()
{
    pthread_mutex_lock(&lock_);
    {
        pthread_mutex_lock(&readLock_);
        sem_wait(&semaphore_);
        pthread_mutex_unlock(&readLock_);

        ++readersActive_;
    }
    pthread_mutex_unlock(&lock_);
}

void PalindromeDatabase::lockReaders()
{
    pthread_mutex_lock(&lock_);
    {
        pthread_mutex_lock(&readLock_);
        sem_wait(&semaphore_);
        pthread_mutex_unlock(&readLock_);

        ++readersActive_;
    }
    pthread_mutex_unlock(&lock_);
}

void PalindromeDatabase::unlockReaders()
{
    sem_post(&semaphore_);

    pthread_mutex_lock(&lock_);
    {
        --readersActive_;

        if (readersActive_ == 0)
            pthread_cond_signal(&wait_);
    }
    pthread_mutex_unlock(&lock_);
}

void PalindromeDatabase::lockWriters()
{
    pthread_mutex_lock(&lock_);
    {
        if (readersActive_ != 0)
        {
            do
            {
                pthread_cond_wait(&wait_, &lock_);
            } while (readersActive_ != 0);
        }

        pthread_mutex_lock(&readLock_);
        for (int i = 0; i < numberOfReaders_; ++i)
            sem_wait(&semaphore_);
        pthread_mutex_unlock(&readLock_);
    }
    pthread_mutex_unlock(&lock_);
}

void PalindromeDatabase::unlockWriters()
{
    for (int i = 0; i < numberOfReaders_; ++i)
        sem_post(&semaphore_);
}
Community
  • 1
  • 1
IAE
  • 2,213
  • 13
  • 37
  • 71
  • 1
    Don't prefer reader threads: this can cause the writer thread to never write and it makes the reader treads read data that is out of date. – stefaanv Dec 21 '11 at 10:11
  • @stefaanv: Yes, as I understand it, most implementations prefer writers as they are rarer and write operations are usually more important. I'm trying to implement all three variants (read, write, no preference) for the sake of my own understanding. Thanks for the heads-up though. – IAE Dec 21 '11 at 10:14

1 Answers1

2

There are quite some issues with your code:

  1. The semaphore is only used by the writer and as such it is meaningless.
  2. While locking for writer, you use the mutex, while unlocking you don't.
  3. The readers signal a changed condition when #readers become zero, and the writer waits for the signal of the condition variable, but it doesn't check the condition.
  4. Upon locking for writer, if #readers is already zero, it will not actually lock.

Having thought about my remark that it is easy, locking is still tricky, I thought about it and I hope I cracked it with this pseudo code, focussing on correct order not the correct notation:

void lockReader()
{
  lock(rdmutex);  // make sure Reader and Writer can't interfere during locking
  lock(wrmutex);  // lock mutex so waitfor can unlock
  while (writer_)
    waitfor(wrcv, wrmutex);  // no active writers

  ++readers_; // at least 1 reader present
  unlock(wrmutex);
  unlock(rdmutex);
}

void unlockReader()
{
  lock(rdmutex);
  bool noReaders = (--readers_ == 0);
  unlock(rdmutex);
  if (noReaders) signal(rdcv); // signal when no more readers
}

void lockWriter()
{
  lock(WritersLock);  // only 1 writer allowed
  lock(rdmutex);  // lock mutex so waitfor can unlock and no interference by lockReader
  while (readers_ != 0)
    waitfor(rdcv, rdmutex);  // wait until no more readers
  lock(wrmutex);
  writer_ = true;  // a writer is busy
  unlock(wrmutex);
  unlock(rdmutex);
  // WritersLock is still locked
}

void unlockWriter()
{
  lock(wrmutex);
  writer_ = false;
  unlock(wrmutex);
  signal(wrcv);  // no more writer (until WritersLock is unlocked)

  unlock(WritersLock);
}

As it turns out, the Qt implementation is simpler, but my algorithm doesn't need to know the maximum number of readers in advance.

stefaanv
  • 14,072
  • 2
  • 31
  • 53
  • Yikes, my mistake with 3 and 4. Should be fixed now. What do you mean, the semaphore is used only by the writer? The reader locks the semaphore, once a single reader has used the semaphore, the write can't acquire all locks simultaneously and must do a cond_wait. Once the last reader has finished, it wakes the writer and it can acquire ALL semaphores. Is this not correct? – IAE Dec 21 '11 at 10:31
  • @SoulBeaver: about 1: my mistake, I overlooked the sem-functions for the reader-lock(unlock), about 4: what happens in lockWriters when (readers_ == 0)? According to me it returns without adjusting the semaphore. about 2: ok, you only use the mutex for the condition variable. – stefaanv Dec 21 '11 at 10:44
  • @SoulBeaver: your current UnlockReader is weird now and doesn't unlock – stefaanv Dec 21 '11 at 10:47
  • My mistake, I put the correction for lockWriters into the wrong function. It should be fixed and corrected now. – IAE Dec 21 '11 at 10:50
  • @SoulBeaver: nope, now you lock in the UnlockReader() function and you introduced readersActive_, probably a copy from your personal code. – stefaanv Dec 21 '11 at 11:43
  • Oh for crying out loud. I've been fussing over my deadlocking program for thirty minutes now only to realize that I pasted not only wrongly on SO, but in my code as well. – IAE Dec 21 '11 at 11:48
  • Actually, you have another deadlock: if lockWriters is waiting on all semaphores (for-loop) and in the meanwhile lockReaders takes one semaphore, lockReaders will block on lock_ and lockWriters will wait for the last semaphore – stefaanv Dec 21 '11 at 11:59
  • 1
    Actually preferring reader is easy with 2 condition variables and a writer-lock: 1. lock reader: wait for no writers (cv1), increase reader 2. unlock reader: decrease reader, signal when no readers (cv2) 3. lock writer: lock writer-mutex, wait for no readers (cv2), set writer, keep writer-mutex locked 4. unlock writer: reset writer, signal no writer (cv1), unlock writer-mutex – stefaanv Dec 21 '11 at 12:11
  • I actually noticed the deadlock too, and I solved it with a second mutex to block the reader from taking a semaphore. Is this code correct? Otherwise, I will try your approach. Thank you so much for your patience and help, I'll be flagging your answer correct after this, I hope that I will finally get it right now since I have all the means and information to do so :) – IAE Dec 21 '11 at 12:18
  • @SoulBeaver: I added pseudo code in my answer showing my previous remark. However it is not tested, so it can still contain errors – stefaanv Dec 21 '11 at 13:41