UNIVERSITY OF WISCONSIN-MADISON
Computer Sciences Department
CS 537
Fall 2007
A. Arpaci-Dusseau
Sample Quiz #3 -- Critical Sections, Locks, and Semaphores

Protecting Critical Sections

The following code samples each specify different implementations for a lock; in all cases, the lock structure is defined, as well as the lock_init(), lock_acquire(), and lock_release() procedures. You should assume that the application calls lock_init() before using the lock and call lock_acquire() before entering the critical section and lock_release() after exiting the critical section. For the implementations that require a tid (i.e., thread id), you can assume that the tid of each thread is either 0 or 1 and that this is available to the function. Hint: Remember to consider how variables are initialized!

For each code sample, answer the following questions:

  1. Does the code guarantee mutual exclusion? (Briefly, why or why not?)
  2. Does the code guarantee progress? (Briefly, why or why not?)
  3. List all other limitations that exist for each implementation. Issues you might consider include (but are not limited to) the following: generality, efficiency, and fairness.
typedef struct {
  int turn;
} lock_t;

lock_init(lock_t *l) {
    l->turn = 0;
}


void lock_acquire(lock_t *l) {
    while (l->turn == (1 - tid));
}

void lock_release(lock_t *l) {
    l->turn = (1 - tid);
}
typedef struct {
} lock_t;

lock_init(lock_t *l) {
}


void lock_acquire(lock_t *l) {
   disableInterrupts();
}

void lock_release(lock_t *l) {
   enableInterrupts();	
}
typedef struct {
   int turn;
   int lock[2];
} lock_t;

lock_init(lock_t *l) {
   l->turn = 0;
   l->lock[0] = l->lock[1] = 0;
}


void lock_acquire(lock_t *l) {
   l->lock[tid] = 1;
   l->turn = 1 - tid;
   while (l->lock[1-tid] && l->turn == (1 - tid));
}

void lock_release(lock_t *l) {
   l->lock[tid] = 0;
}
typedef struct {
   int lock;
} lock_t;

lock_init(lock_t *l) {
   l->lock = 1;
}


void lock_acquire(lock_t *l) {
   while (TestAndSet(lock, 1);
}

void lock_release(lock_t *l) {
   l->lock = 0;
}

Locks

Locks are often implemented by maintaining a list of processes (or threads) that are waiting to acquire the lock. What are all of the advantages of this approach (compared to a correct implementation of a lock that does not use a list)? Be precise in your answer.

Classic Synchronization Problems: Readers and Writers

The following code implements the readers-writers problem with semaphores. (The text book gives an overview of the problem and gives code for a different set of assumptions...) The main thread of the program (not shown) creates many reader and writer threads, starts each of them, and then waits forever for them to terminate by calling pthread_join() appropriately.


// Global variables, shared across reader and writer threads
volatile int sharedBuffer[] = new int[1000];     
sem_t mutex;
sem_t OKToRead;
sem_t OKToWrite;

volatile int ActiveWriters = 0; 
volatile int WaitingWriters = 0;
volatile int ActiveReaders = 0; 
volatile int WaitingReaders = 0;

void reader() {

     sem_wait(&mutex);
     if ((ActiveWriters + WaitingWriters) == 0){
         sem_signal(&OKToRead);
         ActiveReaders++;
     } else {
         WaitingReaders++;
     }
     sem_signal(&mutex);
     sem_wait(&OKToRead);

     // Perform read of sharedBuffer

     sem_wait(&mutex);
     ActiveReaders--;
     if ((ActiveReaders == 0) && (WaitingWriters > 0)) {
          sem_signal(&OKToWrite);
          ActiveWriters++;
          WaitingWriters--;
     }
     sem_signal(&mutex);
}
private void writer() {

     sem_wait(&mutex);
     if ((ActiveWriters + ActiveReaders + WaitingWriters) == 0) {
          sem_signal(&OKToWrite);
          ActiveWriters++;
     } else {
          WaitingWriters++;
     }
     sem_signal(&mutex);
     sem_wait(&OKToWrite);

     // Perform write of dataBuffer 

     sem_wait(&mutex);
     ActiveWriters--;
     if (WaitingWriters > 0) {
         sem_signal(&OKToWrite);
         ActiveWriters++;
         WaitingWriters--;
     } else {
         while (WaitingReaders > 0) {
             sem_signal(&OKToRead);
             ActiveReaders++;
             WaitingReaders--;
         }
     }
     sem_signal(&mutex);
}

You should assume that semaphores have been implemented in a reasonable way. You should assume that all semaphores are implemented with a First-in-First-out (FIFO) list of threads; that is, the thread that has been waiting the longest for a semaphore will be the next one woken up.

You need to answer five questions about this code segment. Each will be worth the same number of points.

  1. What values should each of the semaphores be initialized to?
  2. a) Assume a reader is currently using the buffer.
    If there are both waiting writers and waiting readers, will a writer or a reader get access to the buffer first?
    b) Assume a writer is currently using the buffer.
    If there are both waiting writers and waiting readers, will a writer or a reader get access to the buffer first?
  3. a) Is it possible for readers to starve with this code?
    b) Is it possible for writers?
  4. a) Can the semaphore OKToRead have a value greater than 1?
    b) Can OKToWrite?
  5. Assume there are many readers currently accessing the buffer.
    a) Is the first writer to execute sem_wait(&mutex) guaranteed to be the first writer to use the buffer?

    b) Is the first writer to execute sem_wait(&OKToWrite) guaranteed to be the first writer to use the buffer?