CS111: Lecture 10 Scribe Notes

Synchronization Primitives, Deadlock and File System Performance

By Haokun Luo, Xin Zhang, Xinheng Huang

Contents

1 Synchronization (Continued)
   1.1 Semaphore
     1.1.1 Why Do We Need Semaphore?
     1.1.2 Simple Pseudocode
     1.1.3 One Possible Implementation
     1.1.4 One Prototype of Semaphore
   1.2 API for Condition Variable
   1.3 What can Go Wrong?
2 Deadlock
   2.1 Condition for Deadlock
   2.2 Disciplines for Avoiding Deadlocks
3 Priority Inversion
4 Livelock
5 Event-driven Programming
6 File System Performance Issues
7 Components of Disk Latency

1 Synchronization (Continued)


1.1 Semaphore

A blocking mutex, which allows up to N simultaneous locks. (We can find it in Java standard library!)

1.1.1 Why Do We Need Semaphore?

Let's consider the case of a 1G network interface. Suppose there are 5 zillion threads in total and each thread tries to access to the server. It's impossible for the server to satisfy every request from each thread right now. As a result, we need semaphore to manage the network traffic, and at most N lucky threads get the permission.

1.1.2 Simple Pseudocode

Suppose we have N initial slots. Here is the simple implement of Semaphore:


	While (# of slots == 0)
	    Continue;
	Run the thread;
	# of slots --;

If we have N =1, then we have a standard blocking mutex (also called binary semaphore).

1.1.3 One Possible Implementation

The Data Structure:


	Struct sema {
		int slots;
		process_t * waiting;
		lock_t l ;   // spin lock
	} 

Two Operation Invented by Dijkstra (The "ijk" guy)

Example on implment P and V:


function V(semaphore S):
    Atomically increment S
    S = S + 1;

function P(semaphore S):
    repeat:
        Between repetitions of the loop other processes may operate on the semaphore
        if S > 0:
            Atomically decrement S - note that S cannot become negative
            S = S - 1;
            break;

1.1.4 One Prototype of Semaphore


1 struct pipe{
2 	 bmutext_t m;
3	 char buff[N];
4	 size_t r, w;
5 }
6
7 void writeC ( struct pipe *p , char c){
8     for (;;){
9   	acquire ( &p -> m);		// part of scheduling
10   	if(&p->w - &p->r != m)
11   	    break;
12   	release(&p->m); 		// put to sleep
13    }
14	  &p->buff[&p->w ++ % N ] = c;
15    release (&p ->m );
16 }

1.2 API for Condition Variable

Three important User functions:

  1. 
    wait ( condvar_t *c, bmutex_t *b );
    // hold precondition, otherwise create error
    // release b
    // block until some of thread notifies on C
    // then reacquire b
    // then return
    
    
  2. 
    notify( condvar_t *c );
    // wake up one of the process
    
    
  3. 
    broadcast( condvar_t *c );
    // wake up all of the processes
    
    

Using the API of condition variable, we can rewrite our writeC():


1 void writeC ( struct pipe *p, char c ) {
2	acquire(&p->m);
3	while (&p->w - &p-> r == N ) 
4		wait(&p->nonfull, &p->m);
5	&p->buff [&p->w++% N] = c;
6   notify(&p->nonempty);
7   release(&p->m);
8 }

1.3 What can Go Wrong?

Pipe Example

2 Deadlock


2.1 Condition for Deadlock

2.2 Disciplines for Avoiding Deadlocks

  1. Acquire () checks at runtime for cycles, fails if it would create a cycle
    Prons and Cons:
    + No deadlock
    - Complicates API (acquire() and writeC() need to deal with system fail) + application error == EWOULDDEADLOCK
    - Runtime cost 0 ( length of dependencies ) short in practice
  2. Lock ordering
    Suppose you need to acquire locks &l < &m < &n, grab (l) then grab (m) then grab (n), if any grab() fails, then release the lock.
    Prons and Cons:
    + No deadlock
    + Better runtime cost
    - strict about the order rules (Unfortunately, Linux kernel do not support this)

3 Priority Inversion


Priority inversion is a problematic scenario in scheduling when a high priority task is indirectly preempted by a medium priority task effectively "inverting" the relative priorities of the two tasks. This violates the priority model that high priority tasks can only be prevented from running by higher priority tasks and briefly by low priority tasks which will quickly complete their use of a resource shared by the high and low priority tasks. You can think it as locks + scheduling priorities, and it feels sort of like a deadlock.

Priority Inversion

4 Livelock


A livelock is similar to a deadlock, except that the states of the processes involved in the livelock constantly change with regard to one another, none progressing. In other words, nobody waits but no work gets done. A good example is when requests are sending at 100 Gb/s to a bounded buffer of unhandled requests, the CPU can only process the requests at 10 Gb/s. Thus, it will drop 90% of the request through interrupt handler. The problem comes when the buffer is full, and there are still lots of incoming requests to send interrupts to CPU, then the interrupt handler will chew up the CPU, and nothing will be done.

Livelock Solution:
  1. Disable the interrupt when CPU is busy.
  2. Polling, namely no interrupt at all.

5 Event-driven Programming


Simple Pesudocode:


1  For(;;){
2	Wait for any event e;
3	Act on (e);	// even e must be fast, must not wait
4			// If we have a complex action, we need to split it up.
5			// Each sub-action sets up event handler for next.
6  }

Prons and Cons:
+ no lock, no threads, no scheduling =gt; no deadlock, no race condition
- Difficult to extend to excessively complex application code.

6 File System Performance Issues


There are two types file system: disk and flash.

Disk Speed

7 Components of Disk Latency


Disk Latency

Two costs when reading random sector on a disk:

  1. Seek Time: wait for read head to get to the right track
    Average seek time:
      8.5 ms read // locate at outer part of the disk, which is near the arm
      9.5 ms write // locate at inner part of the disk, which is far from the arm
  2. Average rotational latency = 8.33 ms

Our Goal is to get all those numbers down!

Valid XHTML 1.1