Syllabus   Blank Homework   Quizzes  
Notes   Labs   Scores   Blank

Lecture Notes
Dr. Tong Lai Yu, 2010

  1. Introduction
  2. Processes
  3. Inter Process Communication
  4. Deadlocks
  5. Memory Management
 
  1. File Systems
  2. Protection and Security
  3. I/O Systems


We are what we repeatedly do.
Excellence, then, is not an act, but a habit.

					Aristotle
Interprocess Communication ( IPC )
  1. Race Conditions and Critical Sections

    A Synchronization Problem


    A World War I Fighter Aircraft
  2. Germans: Flew high to gain altitude, turned off engine (propeller stopped) to fire
    machine guns ~ coarse-grained synchronizaion
    ~ mutual exclusion
  3. Germans later developed technology to synchronize between gun firing and whirling
    propeller blades ~ fine-grained synchronization
  4. Synchronization: the use of atomic operations to ensure the correct operation of cooperating processes

    Race condition : an undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time

    Shared resource: critical resource

    Mutual exclusion: Mechanisms that ensure that only one person or process is doing certain things at one time (others are excluded).

    Critical section: A section of code, or collection of operations, in which only one thread may be executing at a given time because the code needs to access a shared resource. (e.g. code segment for printing to a printer )

    Good solution requirements of mutual exclusion

  5. No two processes may be simultaneously inside their critical section
  6. Independent of CPU speed and # of CPU
  7. No process running outside its critical section should block any process
  8. Bounded waiting

  9. Mutual Exclusing for accessing critical section.

  10. Mutual Exclusions

  11. Disabling interrupts

      when a process enters a critical section, it disables all interrupts process i
          while ( 1 ) {
            disable_interrupt();     // lock
            critical_section();
            enable_interrupt();      // unlock
            non_critical_section();
            ....
          }
          

      =>no clock interrupts can occur => no context switch

      problem:

    • if a process forgot to turn on interrupts, other processes can't be executed
    • requires special privileges
    • mulitprocessing -- time-consuming and difficult to disable all interrupts, not appropriate to be used in multicores system

  12. Two-process solutions
      only 2 processes: P0, P1

      strict alternation

      turn = 1 => P1
      turn = 0 => P0
      P0  P1
      while ( 1 ) {
        while ( turn != 0 )
        	;		//wait
        critical_section();
        turn = 1;		//P1's turn now
        non_critical_section();
      }
      	
       
      while ( 1 ) {
        while ( turn != 1 )
      	;		//wait
        critical_section();
        turn = 0;		//P0's turn now
        non_critical_section();
      }
      	

      The non_critical_section() of say P1 could be very long. This may lead to P0 blocking itself.

      Peterson's Solution

      P0  P1
      	//global variables
      	int turn;		//0 or 1
      	int interested[2];	//interested to enter C.S.
      	
      void enter_region0() {
        interested[0] = 1;	//P0 interested in C.S.
        turn = 1;		//P0, not your turn, wait first
        while ( turn == 1 && interested[1] )
          ;    //wait, if the other is interested
      }
      
      void leave_region0() {	//P0 leaving C.S.
          interested[0] = 0;
      }
      	
       
      
      void enter_region1() {
        interested[1] = 1;	//P1 interested in C.S.
        turn = 0;		//P1,not your turn, wait first
        while ( turn == 0 && interested[0] )
          ;    //wait, if the other is interested
      }
      
      void leave_region1() {	//P1 leaving C.S.
          interested[1] = 0;
      }
      	

      Before entering its critical seciton, each process calls enter_regionid() with its own process id. This call will cause it to wait, if need be, until it is safe to enter.

  13. Multiple-process solutions

    Lamport's Bakery Algorithm

    • get a ticket number to purchase baked goods
    • if two processes pick the number at the same time, it does not gaurantee two processes have different #
    • if number( Pi ) < number ( Pj ) serve Pi first
    • if number ( Pi ) = number ( Pj ), then check i, j ( unique process ids ); if i < j, then serve Pi first
    • The notation (a,b) < (c,d) is defined as (a < c) or (a = c and b < d)

      Process i
      
      boolean choosing[n];
      int number[n];
      while (true)
      {
           choosing[i] = true;	//picking a number
      				//max ( number[0], ..., number[n-1] ) + 1
           number[i] = 1 + getmax( number[], n );
           choosing[i] = false;	//finished picking a number
           for (int j=0; j < n; j++ )	//check with others
           {
                while ( choosing[j] )	//don't do anything while someone
                	;		//is choosing a #
                while (( number[j]!=0 ) && ( number[j],j) < ( number[i],i ))
                	;		//check if process j has higher priority 
      				//( lower # )
           }
           critical_section();
           number[i] = 0;		//reset
           non_critical_section();
      }
      	

  14. Use TSL Instruction

    • help from hardware
    • Test-and-Set Lock ( atomic action, i.e. indivisible )
    • Use flag ( lock ) to coordinate access to shared memory, set to 1 by TSL

      while ( 1 ) {
        test_and_set();	//lock ← 1; wait if necessary
        critical_section();
        reset();		//lock ← 0
      }
      
      
      ;each assembly instruction is atomic test_and_set: TSL register, lock //register ← lock, lock ← 1 CMP register, #1 //is lock = 1 ? JE test_and_set //yes, C.S. forbiden, wait RET
      reset: MOV lock, #0 //lock ← 0 RET

  15. Semaphores

    o Generalization: some code sections may be accessed by a limited number of threads

    o A semaphore S is an integer variable, apart from initialization is accessed through standard atomic operations:

  16. wait ( S ): waits for semaphore S to become positive, then decrements it by 1. ( or down( S ), P(S) )
  17. signal ( S ): increments semaphore S by 1.( or up ( S ), V(S) )

    	void wait ( int S ) {	//atomic
    	  while ( S <= 0 ) 
    	    	blocked();		//wait
    	  S = S - 1;
    	}
    
    
    
    
    	
    void signal ( int S ) { //atomic S = S + 1; wakeUpSleeper(); }
    when ( S > 0 ) [
        S = S - 1;
    ]

    when ~ guard
    only when the guarded statement is true,
    will the statements inside the square brackets,
    known as command sequence, be executed!


    [ S = S + 1; ]

  18. o Invented by Dijkstra in 1965

    o Semaphores are simple and elegant and allow the solution of many interesting problems. They do a lot more than just mutual exclusion.

    o Binary semaphores are those that have only two values, 0 and 1. They are implemented in the same way as regular semaphores except multiple signal()s will not increase the semaphore to anything greater than one.

    o Semaphores are not provided by hardware. But they have several attractive properties:

  19. Machine independent.
  20. Simple.
  21. Powerful. Embody both exclusion and waiting but do NOT need to be busy waiting
  22. Correctness is easy to determine.
  23. Work with many processes.
  24. Can have many different critical sections with different semaphores.
  25. Can acquire many resources simultaneously (multiple P's).
  26. Can permit multiple processes into the critical section at once, if that is desirable.
  27. o Here is an example of how semaphores are used to enforce mutual exclusion:

    o When a process executes the wait() operation and finds that the semaphore value is not positive, it must wait.

    Example

    In figure A, there are three semaphores: S1 has a value of zero and process P1 is blocked on S1; S2 has a value of three, meaning three more processes may execute a down operation on S2 without being put to sleep; S3 has a value of zero, and has two sleeping processes, P4 and P5, blocked on it.

    Now let us look at what happens when a process executes an UP on S1. In figure B, process P1 is activated by an UP operation. It executes a DOWN on S1 and enters its critical section. The state of the semaphores after these actions has now changed.

    o C implementation

    	typedef int	process;
    
    	typedef struct {
    	  int		count;
    	  process	Q[MAX_P];	//a queue of processes
    	} Semaphore S;
    
    	Semaphore S;
    	
    void wait ( Semaphore S ) //atomic { if ( S.count > 0 ) //no need to wait --S.count; else { add_process ( S.Q ); //add this process to S.Q sleep(); //block reschedule(); } }
    void signal ( Semaphore S ) //atomic { if ( isEmpty( S.Q ) ) { //no waiters to wake up ++S.count; } else { P = delete_process ( S.Q ); //delete a process P from S.Q wakeup ( P ); //e.g. put P in a ready list ++S.count; reschedule(); //depends on priority of P and //currently running process } }

    o Mutex (Mutual Exclusion)

    If no need to count, may use mutex, a simplified version of semaphore, only has two states: 0 (locked), or 1 (unlocked)
    ( In the textbook, 0 ~ unlocked, 1 ~ locked, reverse of usual semaphore conventions )

      ;each assembly instruction is atomic
      mutex_lock:
          TSL	register, mutex	//register ← mutex, mutex ← 1
          CMP	register, #1	//was mutex 1 (unlock) ?
          JE	ok		//yes, mutex was unlock, so return
          CALL thread_yield   //mutex busy; schedule another thread
          JMP mutex_lock
      ok: RET
      
      mutex_unlock:
          MOV  mutex, #1	//store a 1 (unlock) in mutex
          RET			//return to caller
      	

    o Example
    Solving Producer-consumer problem using semaphore


    /*
      prod-cons.cpp
      Tanenbaum, p. 112. The producer-consumer problem using semaphores.
      Compile:  g++ -o prod-cons prod-cons.cpp -lSDL -lpthread
      Execute:  ./prod-cons
    */
    
    #include <SDL/SDL.h>
    #include <SDL/SDL_thread.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    using namespace std;
    
    #define	N	5			//number of slots in buffer
    SDL_sem	*mutex;				//controls access to critical region
    SDL_sem *nempty;			//counts number of empty slots
    SDL_sem *nfilled;			//counts number of filled slots
    int buffer[N];
    
    
    int produce_item()
    {
      int n = rand() % 1000;	
      return n;
    }
    
    void insert_item ( int item )
    {
      static int tail = 0;
      
      buffer[tail] = item;
      printf("\ninsert %d at %d", item, tail ); 
      tail = ( tail + 1 ) % N; 
    }
    
    int producer ( void *data )
    {
      int item;
    
      while ( true ) {
        SDL_Delay ( rand() % 1000 );//random delay
        item = produce_item();	//produce an item
        SDL_SemWait ( nempty );	//decrement empty count
        SDL_SemWait ( mutex );	//entering critical section
        insert_item ( item );
        SDL_SemPost ( mutex );	//leave C.S.
        SDL_SemPost ( nfilled );	//increment nfilled 
      }
    
      return 0;
    }
    
    int remove_item ()
    {
      static int head = 0;
      int item;
    
      item = buffer[head];
      printf("\nremove %d at %d", item, head );
      head = ( head + 1 ) % N;
      return item;
    }
    
    int consumer ( void *data )
    {
      int item;
    
      while ( true ) {
        SDL_Delay ( rand() % 1000 );//random delay
        SDL_SemWait ( nfilled );	//decrement filled count
        SDL_SemWait ( mutex );	//entering critical section
        item = remove_item ();	//take item from buffer
        SDL_SemPost ( mutex );	//leave C.S.
        SDL_SemPost ( nempty );	//increment count of empty slots
        //can do something with item here
      }
    }
    
    
    int main ()
    {
      SDL_Thread *id1, *id2, *id3;          //thread identifiers
      char *tnames[3] = { "Producer", "Consumer", "Producer2" }; //names of threads
    
      mutex = SDL_CreateSemaphore ( 1 ); 	//initialize mutex to 1
      nempty = SDL_CreateSemaphore ( N );	//initially all slots empty
      nfilled  = SDL_CreateSemaphore ( 0 );	//no slot filled
      id1 = SDL_CreateThread ( producer, tnames[0] );
      id2 = SDL_CreateThread ( consumer, tnames[1] );
      //id3 = SDL_CreateThread ( producer, tnames[2] );
    
      //wait for the threads to exit
      SDL_WaitThread ( id1, NULL );
      SDL_WaitThread ( id2, NULL );
      //SDL_WaitThread ( id3, NULL );
    
      return 0;
    }
      

  28. Condition Variables
  29. For some problems, using semaphores could be complex.
  30. A condition variable is a queue of threads (or processes) waiting for some sort of notifications.
  31. Supported by POSIX and SDL; Win-32 events
  32. A condition variable queue can only be accessed with two methods associated with its queue. These methods are typically called wait and signal. The signal method is called notify in Java.
  33. Threads waiting for a guard to become true enter the queue.
  34. Threads that change the guard from false to true could wake up the waiting threads.
  35. General approach:

    Guarded Command       SDL Implementation
    when ( guard ) [
        statement 1;
        .....
        statement n;
    ]
      SDL_bool condition = SDL_FALSE;
    SDL_mutex *mutex;
    SDL_cond *condVar;
    .....
    SDL_LockMutex ( mutex );
    while ( !condition )
        SDL_CondWait ( condVar, mutex );
    statement 1;
    .....
    statement n;
    SDL_UnlockMutex ( mutex );
     
    //code changing guard from false to true
    .....
      SDL_LockMutex(mutex);
    .....
    condition = SDL_TRUE;
    .....
    SDL_CondSignal ( condVar );
    SDL_UnlockMutex ( mutex );
    ,
  36. To evaluate a guard safely, a thread must mutually exclude the access of all other threads.
  37. While the thread is waiting for the guard to become true, it does not lock the mutex, otherwise other threads cannot change the value of the guard.
  38. Example: Readers-writers problem
    1. Mutual Exclusion required only when is being modified.
    2. Multiple readers can read at the same time.
  39. No simple solution using semaphores, but can be solved easily using condition variables.
  40. Solution presented in guarded commands:
      void reader() 
      {
        when ( writers == 0 ) [
          readers++;
        ]
      
        //read
      
        [readers--;]
      }		
         
      void writer()
      {
        when ( (readers == 0) && (writers == 0) )[
          writers++;
        ]
      
        //write
      
        [writers--;]
      }		
  41. SDL Implementation:
      /*
        readers_writers.cpp
        Compile:  g++ -o  readers_writers readers_writers.cpp -lSDL -lpthread
        Execute:  ./readers_writers
      */
      
      #include <SDL/SDL.h>
      #include <SDL/SDL_thread.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <math.h>
      
      using namespace std;
      
      SDL_bool condition = SDL_FALSE;
      SDL_mutex *mutex;
      SDL_cond *readerQueue;   //condition variable
      SDL_cond *writerQueue;   //condition variable
      
      int readers = 0;
      int writers = 0;
      
      int reader ( void *data )
      {
        SDL_LockMutex ( mutex );
        while ( !(writers == 0) )
          SDL_CondWait ( readerQueue, mutex );
      
        readers++;
       
        SDL_UnlockMutex ( mutex );
        //read
        SDL_Delay ( rand() % 3000);
      
        SDL_LockMutex ( mutex );
        printf("\nThis is %s thread\n", (char *) data );
        if ( --readers == 0 )
          SDL_CondSignal ( writerQueue );
        SDL_UnlockMutex ( mutex );
      }
      
      int writer ( void *data )
      {
        SDL_LockMutex(mutex);
        while ( !( (readers == 0) && (writers == 0) ) )
          SDL_CondWait ( writerQueue, mutex );
      
        writers++;
       
        SDL_UnlockMutex ( mutex );
        //write
        SDL_Delay ( rand() % 3000);
      
        SDL_LockMutex ( mutex );
        writers--;       //only one writer at one time
        printf("\nThis is %s thread\n", (char *) data );
        
        SDL_CondSignal ( writerQueue );
        SDL_CondBroadcast ( readerQueue );
        SDL_UnlockMutex ( mutex );
      }
      
      int main ()
      {
        SDL_Thread *idr[3], *idw[3];          	      //thread identifiers
        char *rnames[] = { "reader 1", "reader 2", "reader 3" }; //names of threads
        char *wnames[] = { "writer 1", "writer 2", "writer 3" }; //names of threads
      
        mutex = SDL_CreateMutex();
        readerQueue = SDL_CreateCond();
        writerQueue = SDL_CreateCond();
        for ( int i = 0; i < 3; i++ ){
          idr[i] = SDL_CreateThread ( reader, rnames[i] );
          idw[i] = SDL_CreateThread ( writer, wnames[i] );
        }
        //wait for the threads to exit
        for ( int i = 0; i < 3; i++ ){
          SDL_WaitThread ( idr[i], NULL );
          SDL_WaitThread ( idw[i], NULL );
        }
        SDL_DestroyCond ( readerQueue );
        SDL_DestroyCond ( writerQueue );
        SDL_DestroyMutex ( mutex );
        return 0;
      }
      	
  42. If too many readers, writers may have starvation. Can be solved with writers-priority.

    Example: Barriers Problem

    • Synchronization for N threads (or processes).
    • Threads enter a barrier but are not allowed to exit until all threads have arrived at the barrier.
    • After threads exit, barrier resets itself so that threads can renter again.
      Solution (First attempt);
        SDL_mutex *mutex;
        SDL_cond  *barrierQueue;    // Condition variable
        const int N = 5;            // Example
        int count = 0;
        
        void barrier()
        {
          SDL_LockMutex ( mutex );
          count++;
          if ( count < N ) {
            SDL_CondWait ( barrierQueue, mutex );
          } else {
            count = 0;    //reset count
            SDL_CondBroadcast ( barrierQueue );  //signal all threads in queue
          }
          SDL_UnlockMutex ( mutex );
        } 

      NOT good solution because:
      • SDL_CondWait might return immediately, depending on the implementation.
        (Also, the thread needs to reacquire the mutex.)
      • Changing if to while won't work because count is reset.

      Solution (Second attempt);

    • Introduce a new guard independent of count ( myEra == era ) to hold its value long enough
      for all threads to exit.
        SDL_mutex *mutex;
        SDL_cond  *barrierQueue;    //condition variable
        const int N = 5;
        int count = 0;
        int era = 0;
      
        void barrier()
        {
          int myEra;               //local variable
          SDL_LockMutex ( mutex );
      
          count++;
          if ( count < N ) {
            myEra = era;
            while ( myEra == era )
              SDL_CondWait ( barrierQueue, mutex );
          } else {
            count = 0;    //reset count
            era++;
            SDL_CondBroadcast ( barrierQueue );  //signal all threads in queue
        }
      
        SDL_UnlockMutex ( mutex );
      } 
    See videos for examples:
  43. A Hello-world example of parallel computing using RPC (Part 1)
  44. A Hello-world example of parallel computing using RPC (Part 2)
  45. Monitors
  46. high-level synchronization,
  47. consists of procedures, shared resources, and administrative data
  48. only one process can be active inside a monitor, i.e. only one process can execute any of the methods at any time ( mutual exclusion )
  49. procedures of monitor can only access data inside monitor
  50. local data of monitor cannot be accessed from outside
  51. When a task attempts to access a monitor method, it is put in the monitor's entry queue. Each monitor has one waiting queue.
  52. Condition variables are part of a monitor. There is a queue for each condition variable.
  53. A task that holds the monitor lock may give it up and enter a condition variable queue by executing the corresponding wait method.
  54. A task that holds the monitor lock may revive a task waiting in a condition variable queue with the notify method of that queue.
  55. The notify method removes one task from the condition variable queue if the queue is not empty.
  56. Processes in the monitor queues are waiting to acquire the monitor lock.
  57. When a task is removed from one of the condition variable queues, it is put in the waiting queue.

    Weakness:

    1. one process active inside monitor => defeats concurrency purpose
    2. nested monitor calls --> deadlock

    o Java Synchronization ~ monitor, no condition variable
    A Java example :

    Click here to see.
  58. Message Passing

    send ( P, &message );	//send a message to process P
    
    receive ( Q, &message );//receive a message from Q
    	

    A communication link has to satisfy the following properties:

    • Processes only need to know each other's identity to communicate.
    • Exactly one link between 2 processes.
    • Link may be unidirectional but usually bidirectional.

      Point-to-Point

      Broadcast
      direct, indirect, blocking (synchronous ), nonblocking (aysnchronous ), zero buffer, finite buffer, infinite buffer

      Example: Producer-consumer problem

      	#define	N 100	//# of slots in buffer
      
      	void producer()
      	{
      	  int item;
      	  message	m;	//message buffer ( ~plate )
      
      	  while ( true ) {
      	    produce_item ( &item );	//generate something to put in buffer
      	    receive ( consumer, &m );	//wait for an empty buffer to arrive
      	    build_message ( &m, item );	//construct a message to send
      	    send ( consumer, &m );	//send item to consumer
      	  }
      	}
      	
      void consumer() { int item, i; message m; //buffer to hold a message for ( i = 0; i < N; i++ ) //send N empty messages send ( producer, &m ); while ( 1 ) { receive ( producer, &m ); //get message containing item extract_item ( &m, &item ); //take item out of message send ( producer, &m ); //send empty buffer reply consume_item ( item ); //consume item } }

    indirect communication

      mailbox ( in UNIX, this is called pipes )

      send ( A, &message )     //send a message to mailbox A

      receive ( A, &message )     //receive a message from mailbox A

    • zero buffering
    • no buffer
    • sender must wait ( block ) until receiver receives a message ( and vice versa )
      i.e. rendezvous
    • if exchanged information is crucial for the computation, sender must be sure message received
      acknowledgement

      e.g. process P → process Q

      send ( Q, &message );
      receive ( Q, &message );

      receive ( P, &message );
      send ( P, "acknowledgement" );

  59. Classical IPC Problems

    The dining philosophers problem ( Dijkstra, 1965 )

    Analysis

    • chopsticks -- shared resources
    • neighbours may compete for chopsticks -- race conditions; so use a binary semaphore mutex to protect the usage of chopstics
    	//philosopher i
    	
    	while ( true ) {
    	  think();
    	  wait ( mutex );	//first check if someone is eating
    	  take_chops( i );	//take left chopstick
    	  take_chops((i+1)%5);	//take right chopstick
    	  eat();
    	  put_chops( i );	//put down left chops
    	  put_chops( (i+1)%5);	//put down right chops
    	  signal( mutex );	//wake up anyone who's waiting
    	}
    	

    O.K. It works! But what's the drawback of the above algorithm?

    Improvement:

    • Use mutex lock to protect each chopstick rather than eating

      deadlock

      starvation ( if we modify the algorithm so that each philospher puts chop back if she could not acquire both; she would wait for a while and try again )

      Solution without deadlock:

    • Use an array, state[] to keep track of whether a philosopher is:
    • EATING
    • THINKING, or
    • HUNGRY
    • (Use a mutex to 'lock' the operation of setting or checking state.)
    • Philosopher i can set state[i] = EATING only if her two neighbours are not eating, i.e. ( state[(i-1)%5] != EATING ) and
            ( state[(i+1)%5] != EATING )
    • Philosopher i uses semaphore s[i] to delay herself if she is hungry but is unable to acquire the chopsticks.

      	//philosopher i
      	#define	LEFT	( i - 1 ) % 5
      	#define	RIGHT	( i + 1 ) % 5
      
      	typedef	int	semaphore;
      	int 		state[5];  //array keeping track of everyone's state
      	semaphore	s[5] = {0, 0, 0, 0, 0};	//one semaphore per philospher
      	semaphore	mutex = 1;
      
      	void philosopher ( int i )
      	{
      	  while ( 1 ) {
      	    think();
      	    pickup ( i );	//pick up chops
      	    eat();
      	    putdown( i );	//put down chops
      	  }
      	}
      	
      void pickup( int i ) { wait ( mutex ); state[i] = HUNGRY; test ( i ); //try to get yourself into EATING state (acquire two chops) signal ( mutex ); wait ( s[i] ); //block if chops were not acquired //this is same as "if ( state[i] != EATING ) wait( s[i] );" // because signal ( s[i] ) would be executed if // state[i] is set to EATING successfully }
      void putdown ( int i ) { wait ( mutex ); state[i] = THINKING; test ( LEFT ); //test left, signal her if she's waiting test ( RIGHT ); //test right, signal her if she's waiting signal ( mutex ); }
      void test ( int i ) { if ( state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING ) { state[i] = EATING; //O.K., i can eat if her neigbour's aren't signal ( s[i] ); //wake up i who is waiting on s[i] } }

    The Sleeping Barber Problem

    • The barber shop has one barber, N chairs for waiting customers.
    • If no customers, the barber sleeps.
    • If a customer arrives and the shop is full, she leaves otherwise she sits on the waiting chairs and wait.
    • A customer has to wake up the barber to serve her.

      /*
        Tanenbaum, p. 131
        barber.cpp
        Compile:  g++ -o barber barber.cpp -lSDL -lpthread
        Execute:  ./barber
      */
      
      #include <SDL/SDL.h>
      #include <SDL/SDL_thread.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <string>
      #include <deque>
      
      using namespace std;
      
      #define	N	5		//number of chairs for waiting customers
      SDL_sem	*mutex;			//controls access to critical region
      SDL_sem *ncustomers;		//# of customers waiting for service
      SDL_sem *nbarbers;		//# of barbers waiting for customers
      int nwaiting = 0;		//# of customers who are waiting ( not being cut )
      bool dinner_time = false;	
      
      void cut_hair()
      {
        int n = rand() % 1000;	
        printf("\nBarber cutting hair");
        SDL_Delay ( n );		//simulate hair cutting
        
      }
      
      int barber ( void *data )
      {
      
        while ( true ) {
          if ( dinner_time &&  nwaiting == 0 ) break;	//end of the day, go dinner
          SDL_SemWait ( ncustomers );	//go to sleep if # of customers is 0
          SDL_SemWait ( mutex );	//acquire access to 'waiting'
          nwaiting = nwaiting - 1;	//decrement waiting customer count
          SDL_SemPost ( nbarbers );	//one barber is ready to cut hair
          SDL_SemPost ( mutex );	//leave C.S.
          cut_hair();		//cut hair ( outside critical section )
        }
      
        printf("\nShop closing! Its dinner dinner time!");
        return 0;
      }
      
      void get_haircut( const char *cname )
      {
        int n = rand() % 1000;	
        printf("\n%s is being served", cname );
        SDL_Delay ( n );		//simulate hair cutting
        
      }
        
      deque<string> cqueue;		//queue for customer names
      
      int customer ( void *data )
      {
        int item;
        SDL_SemWait ( mutex );	//enter critical section 
        string s = string ( (char *) data );
      
        printf("\nnwaiting = %d ", nwaiting );
        if ( nwaiting < N  ) {		//seats available, enter shop
              cqueue.push_back ( s );
      	nwaiting += 1;		//increment count of waiting customers
      	SDL_SemPost ( ncustomers );	//wake up barber if necessary
      	SDL_SemPost ( mutex );		//release access to 'waiting'
      	SDL_SemWait ( nbarbers );	//go to sleep if no barber available
      	string s1 = cqueue.front();
              cqueue.pop_front();
      	get_haircut( s1.c_str()  );		//be seated and be serviced
        } else {			//if no more free chairs, leave
            printf("\nShop full, %s leaving", s.c_str() );
            SDL_SemPost ( mutex );	//shop is full; do not wait
        }
      }
      
      
      int main ()
      {
        SDL_Thread *barber_id;          	//thread identifiers
        SDL_Thread *customer_id[100];	  	//simulate up to one hundred customers
        char tname1[] = { "Barber" }; 	//names of threads
        char tname2[20];
      
        mutex = SDL_CreateSemaphore ( 1 ); 	//initialize mutex to 1
        ncustomers = SDL_CreateSemaphore ( 0 );	//initially no customers waiting
        nbarbers  = SDL_CreateSemaphore ( 0 );	//initially no barber there
        barber_id = SDL_CreateThread ( barber, tname1 );
        for ( int i = 0; i < 50; ++i ) {
          SDL_Delay ( rand() % 500 );		
          sprintf( tname2, "Customer %d", i );
          customer_id[i] = SDL_CreateThread ( customer, tname2 );
        }
        dinner_time = true;		//end of the day, no more customers
        //wait for the threads to exit
        SDL_WaitThread ( barber_id, NULL );
        for ( int i = 0; i < 50; ++i ) 
          SDL_WaitThread ( customer_id[i], NULL );
      
        return 0;
      }
      
      	


    Class Exercises

    1. What is a semaphore?
    2. A thread can be in only one semaphore's waiting queue at a time. True or false?
    3. What is a major benefit of implementing semaphores in kernel?
    4. Consider a semaphore that allows the thread with the highest priority to proceed when UP() is called. What potential problem can this cause?
    5. What could potentially happen if a thread called UP() without having called the down() operation?