COMP 3511: Lecture 15

Date: 2024-10-24 15:15:04

Reviewed:

Topic / Chapter:

summary

❓Questions

Notes

Synchronization Tools
  • Semaphore

    • semaphore : non-negative integer variable
      • a generalized lock
      • first defined by Dijkstra in late 1960s
        • can behave similarly to mutex lock, but has more sophisticated usage
        • min synchronization primitive used in original UNIX
    • two standard operations for modifying: wait() and signal()
      • originally P() and V() for proberen (test) and verhogen (increment) in Dutch
    • must be execute atomically s.t. no more than one process: execute wait() and signal() on same semaphore at the same time
      • idea: serialization
    • semaphore: only accessible by those two operations, except initialization
      void wait(S) {
        while (S <= 0)
          ; // busy wait
        S--;
      }
      void signal (S) {
        S++;
      }
      
  • Semaphore usage

    • counting semaphore: integer value over unrestricted domain
      • can be used: for controlling access to given set of resources
      • of finite number of instances
      • initialized to: no. of resources available
    • binary semaphore: integer value, 0 or 1
      • can be hav like mutex locks
      • πŸ‘¨β€πŸ« but also can be used in different ways
    • can also: solve various synchronization problems
    • consider : sharing a common semaphore synch - initialized to 0
      • following code: ensures that process executes before execute
      P_1: 
        S_1;
        signal(synch);
      P_2: 
        wait(synch);
        S_2;
      
  • Implementation with no busy waiting

    • each semaphore: associated w/ waiting queue
    • each entry in waiting queue: w/ two data ideas
      • value (in integer)
      • pointer to next record on queue
    • two operations
      • block: place process invoking operation to (appropriate) waiting queue
      • wakeup: remove one of process in waiting queue
        • place it on ready queue
    • semaphore: may be negative
      • w/ classical busy-waiting semaphore: impossible
    • if semaphore value = negative:
      • magnitude = no. of processes currently waiting on semaphore
  • Signal semaphore

    • code
      typedef struct{
          int value; // if negative: processes being waiting
          struct process *list;
      } semaphore;
      wait(semaphore *S) {
          S->value--;
          if (S->value < 0) {
              add this process to S->list;
              block();
          }
      }
      // signal: has chance of changing condition
      signal(semaphore *S) {
          S->value++;
          if (S->value <= 0) {
              remove a process P from S->list;
              wakeup(P);
          }
      }
      
    • notice:
      • increment & decrement: done before checking semaphore value
      • block(): suspends the process & invokes it
      • wakeup(P): resumes execution of a suspended process
  • Deadlock and starvation

    • semaphore initialized w/ (binary semaphore)
      • forces atomic operation
    • deadlock: two / more processes: waiting indefinitely
      • for an event: that can be caused oy only one of waiting processes
      • e.g. : with half-torn treasure map
        • each: request the other part of map
          • without giving up the piece they are holding
          • won't happen!
    • if : two semaphores initialized to :
      wait(S);wait(Q);
      wait(Q);wait(S);
      signal(S);signal(Q);
      signal(Q);signal(S);
      • : waiting for to execute signal(Q)
        • and : waiting for to execute signal(S)
        • deadlock!
    • starvation: indefinite blocking
      • process: may never being removed from semaphore queue
        • then: suspended
      • e.g. removing process from queue using LIFO
        • w/ certain priorities
Synchronization Example
  • Bounded-buffer problem

    • counting semaphore for solving a problem!
    • buffers, each holding one item
    • semaphore mutex initialized to 1
      • binary
    • semaphore full initialized to 0
      • no. of full slots in buffer
    • semaphore empty initialized to
      • signal(mutex);
      • no. of empty slots in buffer
    • structure of the producer process
      do {
          // produce item
          wait(empty);
          wait(mutex); // guarantee mutual execution
          // add next produced to buffer
          signal(mutex);
          signal(full); // no. of items in buffer
      } while (true);
      
    • structure of the consumer process
      do {
          // remove item from budder to variable
          wait(full);
          wait(mutex); // guarantee mutual execution
          // consume next item from buffer
          signal(mutex);
          signal(empty);
      } while (true);
      
  • Readers-writers problem

    • data set: shared among a no. of concurrent processes
      • readers: only read the data, without performing any updates
      • writers: can both read & write
    • problem: allow multiple readers to read the data set at the same time
      • however, at most one writer can access shared data at a time
    • several variations on treatment of readers & writers
      • w/ different priorities
    • simplest solution: requiring no reader be kept waiting
      • unless: writer as already gained access to shared data
        • shared data update by writers: can be delayed
          • as: people are reading (outdated) version
        • gives: readers priority in accessing shared data
    • shared data
      • data set
      • semaphore rw_mutex initialized to 1
      • semaphore mutex initialized to 1
      • semaphore read_count initialized to 0
    • for writer: guarantee mutual exclusion
      do {
          wait(rw_mutex);
          // writing
          signal(rw_mutex);
      } while (true);
      
    • for reader: first reader can take over
      • prevent another writer from reading it!
      do {
          wait(mutex); // exclusiveness of following snippet
          read_count++;
          if (read_count == 1)
              wait(rw_mutex);
          signal(mutex);
          // reading performed
          wait(mutex);
          read_count--;
          if (read_count == 0)
              signal(rw_mutex);
          signal(mutex);
      } while (true);
      
    • rw_mutex: controls access to shared data for writers & first reader
      • last reader leaving: release the lock (for potential writer)
    • mutex: controls the access of readers, to shared variable read_count
    • writers: wait on rw_mutex
      • first reader gain access to critical section: also waiting on rw_mutex
      • all subsequent readers: wait on mutex (held by the first reader)
  • Reader-writer problem variations

    • first variation: no reader kept waiting: unless writer gained access
      • simple, yet might result in starvation of writers
        • thus: potential & significant delay of the object's update
    • second variation: one writer is reader: 'needs' perform update first
      • when a writer: waiting for access (either another writer or readers occupying it)
      • no new readers: can start reading
    • either solution: may result in starvation
      • problem: can be solved partially by kernel's reader-writer locks
        • multiple processes: permitted to acquire lock in read mode
        • yet: only one permitted to acquire lock in write mode
      • i.e. specifying: the mode of the lock
Synchronization by OSes
  • by: Solaris, Windows XP, Linux, Pthreads
  • Solaris

    • implements: variety of locks for support multi-tasking, multi-threading, and multi-processing
    • uses: adaptive mutex for efficiency
      • when protecting data from short code segments (less than a few hundred instructions)
      • starts as: standard semaphore implemented as a spinlock in multiprocessor system
      • lock held by a thread running on another CPU: spins to wait for lock
      • if held by non-run-state thread: block & sleep waiting
        • for signal of lock being released
    • condition variables: to be discussed
      • wait until condition satisfies
      • uses: readers-writers locks when longer sections of code need access to data
        • used to protect frequently-accessed data
          • used in read-only manner
        • relatively expensive to implement
  • Windows synchronization

    • kernel: using interrupt masks to protect: access to global resources in uni-processor systems
    • kernel: uses spinlocks for multi-processor systems
      • for efficiency: threads are never preempted when holding a spinlock
    • thread synchronization: outside the kernel (user mode)
      • Windows: provide dispatcher objects
      • threads: synchronize according to several different mechanism
        • mutex locks, semaphores, events, timers
    • events: similar to condition variables; notify waiting thread when condition occurs
    • timers: used to notify one / more thread that specific amount of time has expired
    • dispatcher objects: either signaled-state (object available) or non-signaled state (occupied & block / wait)
  • Linux synchronization

    • prior to kernel 2.6: disables interrupts to implement short critical sections

      • 2.6 and later: fully preemptive kernel
    • provides:

      • semaphores
      • spinlocks (for multi-processor)
      • atomic integer, and math operations involving it
      • reader-writer locks
    • on single CPU: spinlocks replaced by enabling / disabling kernel preemption

    • atomic variables: like atomic_t

      • e.g. variable atomic_t counter; int value;
      atomic operationeffect
      atomic_set(&counter, 5);counter = 5
      atomic_add(10, &counter);counter = counter + 10
      atomic_sub(4, &counter);counter = counter - 4
      atomic_inc(&counter);counter = counter + 1
      value = atomic_read(&counter);value = 12
  • POSIX synchronization

    • POSIX API: provides
      • mutex locks
      • semaphores
      • condition variables
    • widely used on UNIX, Linux, an MAcOS
    • creating & initializing the lock
      #include <pthread.h>
      
      pthread_mutex_t mutex;
      
      /* create and initialize the mutex lock */
      pthread_mutex_init(&mutex, NULL);
      
    • acquiring and releasing the lock
      /* acquire the mutex lock */
      pthread_mutex_lock(&mutex);
      
      /* critical section */
      
      /* release the mutex lock */
      pthread_mutex_unlock(&mutex);
      
    • POSIX condition variables
      • associated w/ POSIX mutex lock to provide: mutual exclusion
      • πŸ‘¨β€πŸ« must be in combination
      • modification of condition variable itself: not guaranteed to be exclusive
        • mutex lock needed
      pthread_mutex_t mutex;
      pthread_cond_t cond_var;
      
      pthread_mutex_init(&mutex, NULL);
      pthread_cond_init(&cond_var, NULL);
      
      • waiting for condition a==b to be true:
        pthread_mutex_lock(&mutex);
        while (a != b)
          pthread_cond_wait(&cond_var, &mutex);
        
        pthread_mutex_unlock(&mutex);
        
        • pthread_cond_wait: in addition to putting the calling thread to sleep
          • release the lock when putting said caller to sleep
          • no other signal: can acquire lock / signal it to wake up
        • it also: release on mutex
          • thus: others can modify the condition..?
        pthread_mutex_lock(&mutex);
        a = b;
        pthread_cond_signal(&cond_var);
        pthread_mutex_unlock(&mutex);
        
        • when signalling: make sure to have the lock held
          • ensures: there is no race condition
        • before returning & being waked up: pthread_cond_wait re-acquires the lock
          • ensures: any time: waiting thread is running "between the lock acquired in the beginning"
          • lock: release at the end
    • πŸ‘¨β€πŸ« there are situation mutex can replace condition variable
      • but there are also situations it can't