CS372H Spring 2011 Homework 4 Solutions

Problem 1

Consider a uniprocessor kernel that user programs can trap into using system calls. The kernel receives and handles interrupt requests from I/O devices. Would there be any need for critical sections within that kernel?

Yes. Assume a user program enters the kernel through a trap. While running the operating system code, the machine receives an interrupt. Now, the interrupt handler may modify some global data structure that other kernel code was trying to modify. Therefore, while there is only one thread that runs inside the kernel at any given time, the kernel may not be re-entrant if access to global data structures is not protected through the use of appropriate mutexes. Of course, since an interrupt handler should never sleep, the "mutex" here on a single processor machine might be implemented as follows: acquire() means "disable interrupts", and release() means "enable interrupts".

Problem 2

Show that the following algorithm is a correct solution to the critical section problem for two processes (satisfies the 3 conditions discussed in class). The solution for process Pi (i = 0 or 1) with Pj(j = 1 or 0) is shown below:
flag[2];
int turn = 0;

flag[i] = 1;
while (flag[j])
{
    if (turn == j)
    {
        flag[i] = 0;
        while(turn == j)
            ;
        flag[i] = 1;
    }
}

/* enter C.S. */
/* exit C.S. */
turn = j;
flag[i] = 0;

This algorithm is called Dekker's solution.

1. Mutual exclusion:
There are two cases to consider:
a. A process is inside the C.S.: Without loss of generality, assume process j is inside the C.S. Before entering
the C.S. the process sets its own flag to 1. If process i tries to enter the C.S. it will see that flag[j] is up and gets caught up in the while loop. It will continue in the while loop until the other process sets its own flag to 0, which happens only at the end of the C.S.
b. Two processes are trying to enter simultaneously: In this situation, if both processes reach their respective while loop at the top, then the variable turn will ensure that only one of them passes through. The variable turn is alternating between the allowing either process, and is only modified at the exit of a C.S.

2. Progress:
There are two cases to consider:
a. One process is trying to enter with no competition: In such a case, the flag of the other process is down, and the process goes past the while look into the critical section directly.
b. Two processes are trying to enter simultaneously. In this case if the first process is trapped into the while loop, then the variable turn will make one of the two variables lower its flag and goes into a loop waiting for the variable turn to change (the inner while loop). The other process whose turn is set by the variable turn will be able to get through.

3. Bounded Waiting:
Assume there is a process blocked inside the inner while loop, while another process is in C.S. In such a case, if the process inside the critical section tries to re-enter, it will be blocked because on exit of the C.S. it has already set the variable turn to point to the other process. Therefore, the process that just got out of the C.S. will be forced to wait for its own turn. So, bounded waiting is taken care of.

Problem 3

Pat Hacker is a hardware designer who came up with a great idea for a hardware instruction that Pat claims can help in solving the critical section problem. It is called atomic counters. An atomic counter is a variable in memory that can be sampled and incremented in one atomic operation. Also, it can be reset to 0. That is, the two operations allowed on the shared variable aare:

        j = a++;         // execute in one atomic operation
        a = 0;

Either come up with a solution to the C.S. problem using this facility or show that it cannot help.

This hardware solution can help implement a solution to the C.S. as follows:

// Shared state

atomic inside = 0;            // inside is an atomic counter

// Per-thread state
int a;

retry:
a = inside++;
if(a != 1)
  goto retry;
/* enter C.S. */
...
/* exit C.S. */
inside = 0;
 
Note that this counter could wrap around, causing a bug in the above solution to mainfest. If we permit an additional operation to read the value, then we can do:

// Shared state

atomic inside = 0;            // inside is an atomic counter

// Per-thread state
int a;

retry:
a = inside++;
if(a != 1){
 while(inside != 0){
  ; // spin
 }
  goto retry;
}
/* enter C.S. */
...
/* exit C.S. */
inside = 0;
 

If we don't want to add such an operation, does the following work?

// Shared state
int delay = 0;
atomic inside = 0;            // inside is an atomic counter
// Per-thread state
int a;

retry:
while(delay){
  // wait
}
delay = 1;
a = inside++;
if(a != 1){
  goto retry;
}
/* enter C.S. */
...
/* exit C.S. */
inside = 0;
delay = 0;
 

Problem 4

A common technique to achieve mutual exclusion in a uniprocessor kernel is to disable interrupts before getting inside a critical section and enable them after exiting a critical section. Explain why this technique works.

A uniprocessor kernel can be entered simultaneously through a trap, and through interrupts. When a thread is running inside the kernel, it could get pre-empted if the machine receives an interrupt. Thus, the interrupt handlers may call functions that access and modify global data structures. There may be a situation where the thread that was running inside the kernel when the interrupt occurred may be modifying the same data structures. This creates a nasty form of concurrency. A standard solution thus is to cut the reason for this concurrency at the source, i.e. disable interrupts which are the cause for pre-empting a thread when it is running inside the kernel. Needless to say, this form of concurrency control is rather crude, and it cannot scale to multiprocessors.

Problem 5

Following is an implementation of a stack data structure for a multithreaded application. Identify the bugs, if any, and correct them.

#include "Exception.h"
#include "Semaphore.h"
#include <iostream.h>

const MaxStackSize = 100;

class Stack            // throws an exception object when popping an empty stack, and when pushing into a full stack
{
private:
    int s[MaxStackSize];
    int stackp;                        // stack pointer
    Exception * e;                  // For error handling
    Semaphore * sem;           // For mutual exclusion
public:
    Stack();
    ~Stack()        {};
    int Pop(void);
    void Push(int item);
};

Stack::Stack()
{
    stackp = MaxStackSize;
    e = new Exception();
    sem = new Semaphore(1);
}

int Stack::Pop(void)
{
    P(sem)
    if(stackp == MaxStackSize)
        {
            e->SetErrorMsg("Popping empty stack");
            e->SetErrorLocation("Stack::Pop()");
            throw(e);
Error: Before throwing the exception, we must release the lock (i.e. V(sem)), or
the stack object will not be accessible to any process any time in the future.
        }
    V(sem);
    return s[stackp++];
Error: We are incrementing stackp after releasing the lock!!
}

void Stack::Push(int item)
{
    P(sem)
    if(stackp == 0)
        {
            e->SetErrorMsg("Pushing to a full stack");
            e->SetErrorLocation("Stack::Push()");
            throw(e);
Error: Before throwing the exception, we must release the lock (i.e. V(sem)), or
the stack object will not be accessible to any process any time in the future.
       }
    s[--stackp] = item;
    V(sem);
}
 

Problem 6

Consider a system with three smoker processes and one agent process. Each smoker continuously rolls a cigarette and then smokes it. But to roll and smoke a cigarette, the smoker needs three ingredients: tobacco, paper, and matches. One of the smoker processes has paper, another has tobacco, and the third has matches. The agent has an infinite supply of all three materials.

The agent places two of the ingredients on the table. The smoker who has the remaining ingredient then makes and smokes a cigarette, signaling the agent on completion. The agent then puts out another two of the three ingredients, and the cycle repeats.

Assume the agent calls the procedure

void chooseIngredients(int *paper, int *tobacco, int *match);
to randomly select 2 of the 3 ingredients. The routine randomly sets 2 of the values to "1" and one of the values to "0". You don't have to write this routine.

Write a program to synchronize the agent and smokers.

  1. What synchronization and state variables will you use in this problem? (For each variable, indicate the variable's type, the variable's name, its initial value (if any), and a short comment describing the variable's purpose.

    Variable Name        Variable Type        Initial Value        Description

  2. Write the routines Agent() and matchSmoker() (the routine for the smoker that has lots of matches.) You don't have to write the routines paperSmoker() or tobaccoSmoker(), but your solution should be general enough so that those routines would be simple variations of matchSmoker().

1.
 

 Variable Name Variable Type Initial Value Description
lock Mutex Lock
 
-
To  protect shared state variables associated with condition variables(
e.g.,.Tobacco, Match, Paper)
IngredientReady ConditionVariable
-
ConditionVariable to coordinate when smokers
consume Ingredients
IngredientConsumed ConditionVariable
-
ConditionVariable to coordinate when agent creates new Ingredients
Tobacco int
0
indicate if Tobacco on the table is created or consumed
Match int
0
indicate if Match on the table is created or consumed
Paper int
0
indicate if Paper on the table is created or consumed

2. 

void Agent()
{
   lock.acquire();
   while(1){
     while (!((Tobacco == 0) &&  (Match == 0) && (Paper == 0))){
        IngredientConsumed.wait(&lock);
     }
     chooseIngredients(&Tobacco, &Match, &Paper);
     IngredientReady.broadcast(&lock);
   }
   lock.release();
}
 

void matchSmoker()
{
   lock.acquire();
   while(1){
     while( !( (Tobacco ==1) && (Paper ==1) )){
       IngredientReady.wait(&lock);
     }
    //     smoking;
    Tobacco = 0;
    Paper = 0;
    IngredientConsumed.signal(&lock);
   }
   lock.release();
}

Problem 7

Write a solution to the dining philosophers using locks and condition variables. You may wish to use the sthread wrapper in the lab T source. Your solution must prevent philosopher starvation. Follow the coding standards discussed in class. On the exam, you will be required to follow these coding standards in all questions of this type.

/*
 * Each philosopher is a thread that does the following:
 */
philosopherMain(int myId, Table *table){
  while(1){
    table->startEating(myId);
    eat();
    table->doneEating(myId);
  }
}





typedef enum stick_status{STICK_FREE, STICK_USED} stick_status;



/*
 * First solution. It is safe but not completely
 * live in that it does not guarantee freedom
 * from starvation.
 */


public class Table{
  public:
  Table(int nSeats);
  void startEating(int philosopherId);
  void doneEating(int philosopherId);

  private:
  mutex lock;
  cond  cv;
  int nseats;
  stick_status stickStatus[];
}


Table::Table(int ns)
{
  int ii;
  nseats = ns;
  stickStatus = (stick_status *)malloc(nseats * sizeof(stick_status));
  for(ii = 0; ii < nseats; ii++){
    stickStatus[ii] = STICK_FREE;
  }
}

void
Table::startEating(int id)
{
  lock.acquire();
  while(stickStatus[stickLeft(id)] == STICK_USED ||   
        stickStatus[stickRight(id)] == STICK_USED){
     cv.wait(&lock);
  }
  stickStatus[stickLeft(id)] = STICK_USED;
  stickStatus[stickRight(id)] = STICK_USED;
  lock.release();
}

void
Table::doneEating(int id)
{
  lock.acquire();
  stickStatus[stickLeft(id)] = STICK_FREE;
  stickStatus[stickRight(id)] = STICK_FREE;
  cv.broadcast(&lock);
  lock.release();
}


/*
 * Second solution. A philosopher should eat if there
 * is no conflict for the chopsticks. But, if two philosophers
 * are waiting for the same chopstick, the one that
 * has been waiting longer should go.
 *
 */

const static int NOT_WAITING = -999;

public class Table{
  public:
  Table(int nSeats);
  void startEating(int philosopherId);
  void doneEating(int philosopherId);

  private:
  mutex lock;
  cond  cv;
  int nseats;
  stick_status stickStatus[];
  int entryTime[];
  int currentTime;

  int okToGo(int id);
}


Table::Table(int ns)
{
  int ii;
  nseats = ns;
  stickStatus = (stick_status *)malloc(nseats * sizeof(stick_status));
  entryTime = (int *)malloc(nseats * sizeof(int));
  for(ii = 0; ii < nseats; ii++){
    stickStatus[ii] = STICK_FREE;
    entryTime[ii] =  NOT_WAITING;
  }
  currentTime = 0;
}

void
Table::startEating(int id)
{
  lock.acquire();
  entryTime[id] = currentTime;
  currentTime++;
  while(!okToGo(id)){
     cv.wait(&lock);
  }
  stickStatus[stickLeft(id)] = STICK_USED;
  stickStatus[stickRight(id)] = STICK_USED;
  entryTime[id] = NOT_WAITING;
  lock.release();
}

void
Table::doneEating(int id)
{
  lock.acquire();
  stickStatus[stickLeft(id)] = STICK_FREE;
  stickStatus[stickRight(id)] = STICK_FREE;
  cv.broadcast(&lock);
  lock.release();
}

int
Table::okToGo(int id)
{
  assert(lock is held on entry);
  /*
   * OK to go if both left and right sticks
   * are free AND for each stick my neighbor
   * is not waiting, or my neighbor's waiting 
   * number is larger than mine
   */
  if(stickStatus[stickLeft(id)] != STICK_FREE){
    return 0;
  }
  if(stickStatus[stickRight(id)] != STICK_FREE){
    return 0;
  }
  if(entryTime[seatRight(id)] != NOT_WAITING
     &&
     entryTime[seatRight(id)] < entryTime[id]){
    return 0;
  }
  if(entryTime[seatLeft(id)] != NOT_WAITING
     &&
     entryTime[seatLeft(id)] < entryTime[id]){
    return 0;
  }
  return 1;
}

Problem 8

Implement the function
void parallelDo(int n, void *(function(void *)), void *arg[])
parallelDo spawns off N worker threads. function is a pointer to a function, and arg[] is an array of arguments to that function (e.g., worker thread i is to execute (*function)(arg[i])). parallelDo returns once all worker threads have finished running the function.

Assume you are given mutex locks and condition variables with standard public interfaces.

Assume that you are given functions creating and destroying threads that are similar to those discussed in class:

void sthread_create(sthread_t *thrd,

void *(start_routine(void*)),
void *arg0ToStartRoutine,
void *arg1ToStartRoutine,
...); // Pass as many args as you like
void sthread_exit(void);

Implement this parallelDo function. Follow the coding standards specified in the handout. Important: follow an object-oriented style of programming - encapsulate all shared states in an object that manages access to it. Solution: please see this PDF file.