CS 162
Spring 1996

Homework 3 -- sample solution and discussion

1 Laundromat: parallel allocators

  // semaphore 'nfree' initialized to NMACHINES,
  // boolean 'available' array initialized to all TRUE.

  int allocate() //Returns index of available machine.
  {
    P(nfree);    // Wait until a machine is available.
    for (int i=0; i < NMACHINES; i++) {
      if (available[i] == TRUE) {
        available[i] = FALSE;
        return i;
      }
    }
  }

  void release(int machine) // Releases machine.
  {
    available[machine] = TRUE;
    V(nfree);
  }

1.a Explain the error

As the code stands, the semaphore nfree properly tracks the availability of machines. However, in addition to identifying when a machine is available, allocate must identify which machine may be used. The code searches a shared array, and a race-condition allows concurrent searching threads to erroneously find and manipulate the same free element.

1.b Eliminate the error

An immediately apparent solution is to force atomicity on the entire array search, thereby eliminating the race-condition entirely:

  Lock available_mutex("available array mutex");

  int allocate() // Returns index of available machine.
  {
    P(nfree);    // Wait until a machine is available.
    available_mutex.Acquire();  // begin atomic region
    for (int i=0; i < NMACHINES; i++) {
      if (available[i] == TRUE) {
        available[i] = FALSE;
        available_mutex.Release();  // end atomic region
        return i;
      }
    }

    // if we get this far, a bad call V(nfree) has been made somewhere!
    assert(FALSE);
  }

this synchronization is stronger than necessary. As the assert statement above suggests, an important invariant is provided by the nfree semaphore: a search of the available array will not commence unless there is a free machine with which the search can terminate. One can use this result to show that for any n threads concurrently executing the search code in allocate, there are at least n array entries marked TRUE.

Because each searching thread is guaranteed a free entry, there is no need to completely serialize the searches. The only place atomicity is required is in the span while reading and updating an entry within the loop, so that there is not a race-condition in the search termination. An alternate solution:

  Lock available_mutex("available array mutex");

  int allocate() // Returns index of available machine.
  {
    P(nfree);    // Wait until a machine is available.
    for (int i=0; i < NMACHINES; i++) {
      available_mutex.Acquire();   // begin atomic region
      if (available[i] == TRUE) {
        available[i] = FALSE;
        available_mutex.Release(); // exit 1 from atomic region
        return i;
      }
      else {
        available_mutex.Release(); // exit 2 from atomic region
      }
    }
  }

note the need to carefully maintain the lock along all execution paths in the atomic region.

Observe that neither solution is inherently better than the other, but rather that each makes different trade-offs in performance. One must weigh the drawbacks of the first solution's non-interleaved searches against the second solution's more frequent synchronization operations in deciding which solution is most appropriate in a given context.

2 Making water: rendezvous

For every triple of two hReady calls and an oReady call, exactly one call to makeWater should be made. In addition, a call to oReady or hReady should return only after successfully forming such a triple. This is analogous to send/receive pairs with synchronizing ports.

  Semaphore hArrived(0);   // for oxygen threads to wait on
  Semaphore hCanLeave(0);  // for hydrogen threads to wait on
  Semaphore oMutex(1);     // to prevent multiple oxygen threads from 
                           // fighting for hydrogen.

  void hReady() {
    hArrived.V();   // announce arrival

    hCanLeave.P();  // wait until we're allowed to leave
  }

  void oReady() { 
    oMutex.P();     // make sure only one oxygen claims arriving hydrogens
                    // to avoid deadlock.
    hArrived.P();   // wait for a hydrogen thread to arrive
    hArrived.P();   // wait for another hydrogen thread to arrive
    oMutex.V();     // allow another oxygen to start...

    makeWater();    // form water with those two atoms and ourself
                    // this assumes makeWater is thread-safe.

    hCanLeave.V();  // allow a hydrogen thread to leave
    hCanLeave.V();  // allow another hydrogen thread to leave
  }

because the oxygen thread is unique in any water-forming triple, it can most easily perform as the controlling thread for the interaction. Were we to have one of the hydrogen threads direct the interaction and call makeWater, we would have to implement a more elaborate election to assign the asymmetric roles. The mutex is required to make sure one oxygen really controls the show; if more than one started counting hydrogens, we could have deadlock.

However, this solution does not really prevent starvation; depending on the implementation of semaphores used, a call to oReady might block indefinitely at the call to oMutex.P() while threads subsequently calling oReady continually get the mutex first!

The astute reader might notice that this simple fact prevents any semaphore-based solution from preventing starvation: regardless of what queueing policy we try to implement, a general locking scheme cannot be constructed to maintain the shared queue without subverting the entire point of having the queue. In order to queue up, a thread must eventually grab a lock, at which point it might starve. As a special case, it is worth noting that a solution can be found if the total number n of participating threads is known. A sequence of n-1 semaphores can be arranged with decreasing initial counts n-1, ..., 1 such that a FIFO queue is simulated as each thread decrements the semaphores in the same decreasing order (and get blocked at different points in the sequence).

The only way to prevent starvation with semaphores is to proscribe semaphore implementations which do not use a FIFO policy. As long as semaphores behave with FIFO ordering, starvation-free programs can be written (the solution given above is sufficient under this condition).