Return to the Lecture Notes Index

Lecture 6 (January 28, 2000)

Many thanks to Jason Flinn for his contribution to today's notes. -GMK

More Mutual Exclusion

The First Solution -- Dekker's Algorithm
Entry:
  flag[i] = true;

  while (flag[j])
  {
    if (turn == j)
    {
      flag[i] = false;
      while (turn == j)
      ;
      flag[i] = true;
    }

  }
  
Exit:
    turn = j;
    flag[i] = false;

This solution is a bit complex, but correct. Let's convince ourselves of this.

Mutual exclusion is not violated. Consider the case where one thread enters the critical section before the other. This thread sets its flag to true. The other thread will loop in the while loop until the other thread sets its own flag in its exit section. So the second thread can't enter until the first exits.

In the event of tie, tunr will be set so that only one of them can enter. The other thread will get caught in the innermost while loop. We don't know which thread will get turn, but it can't be both. In the exit section, turn will be set to allow the other one to exit. Again, one thread can't enter until the other exits. Mutual exclusion is satisfied.

Progress isn't violated, because the resource is never available, but guarded. If there is only one thread that wants the thread, the flag variable is a non-factor and the wanting thread can enter. In the other case, the turn variable and the inner while loop wille sure that one of the two thread gets into the critical section. The progress condition is satisfied.

Bounded wait isn't violated. Assuming there is no competition for the resource, this case is trivial. The thread can immediately enter (see discussion in progress). The other case of interest is ensuring that the wait is bounded if both processes want the critical section. In this case we need to ensure that one process can't keep winning forever. This is satisfied, because as a thread exits, it gives the other process the turn. The other process can immediately enter. And and since the other process's flag is set, the first process can't re-enter first.

The Simple Solution -- Peterson's Algorithm
Entry:  flag[i] = true;
        turn = j;
        while (flag[j] && turn=j) {
                ;

Exit:   flag[i] = false; 
At last, we have a correct solution.

Mutual exclusion can't be violated, because for a process to enter the critical section, either (flag[i] == false) or (turn != j).

We use the flag variable as in Broken Solution #3 to ensure mutual exclusion. But we add the use of the turn variable to break deadlock. Now if both processes claim that it is the other process's turn because both have set their flag, the turn variable can break the tie.

But the turn variable can't break the progress requirement, as it did in Broken Solution #1, because it is only used in the event of a tie. And if both processes want the critical section, we don't have to worry about one process holding up the other, because it doesn't need its turn, yet.

Multi-Process Solution (Lamport's Bakery Algorithm)
int choosing[N] = {flase, ...}
int number[N] = {0,...}

entry:	choosing[i] = true;
	new = 0;
	for (x = 0; x < N; x++) 
		if (number[x] > new) 
			new = number[x];
	number[i] = new+1;
	choosing[i] = false;

	for (x=0; x < N; x++) {
		while (choosing[x])
                ;
		while (number[x] != 0 && 
		       (number[x] < number[i] || 
                        (number[x]==number[i] && x < i)));
	}

exit: number[i] = 0;
This algorithm works by giving each thread a number as it enters the competition for the critical section. Threads gain access to the critical resource in order of this number. The number assigned to the thread is higher than the number assigned to any thread thus far.

The assignment of this number is not protected, so two threads can get the same number. In the event of this tie, the thread with the lowest ID (position in the array of threads, the number[] array) will enter the critical section first.

While this may not be fair, it doesn't have to be. The bounded wait condition doesn't require fairness, just determinism. This approach guarantees that each thread will eventually get into the critical section. Although part of the decision about when a thread will enter the critical section is based on an arbitrary factor (thread id), the threads position in the line is determined when it enters. This means that another thread cannot "cut" in line and violating the bounded wait requirement.

Hardware Support For Synchronization

Disabling Interrupts

Although we have learned how to synchronize any arbitrary number of processes without hardware support, we have also learned that this is a messy business. As with many other aspects of software, hardware support can make it both simpler for the programmer and more efficient.

One rudimentary form of synchronization supported by hardware is frequently used within the kernel: disabling interrupts. The kernel often guarantees that its critical sections are mutually exclusive simply by maintaining control over the execution. Most kernels disable interrupts around many critical regions ensuring that they can not be interrupted. Interrupts are reenabled immediately after the critical section. Unfortunately, this approach only works in kernel mode and can result in a delayed service of interrupts -- or lost interrupts (if two fo the same type occur before interrupts are reenabled). And it only works on uniprocesors, becuase diabling interrupts can be very time-consuming and messy if several processors are involved.

Special Instructions

Another approach is to build into the hardware very simple instructions that can give us a limited guarantee of mutual exclusion in hardware. From these small guarantees, we can build more complex constructs in software.

Test-and-Set

One such instruction that is commonly implemented in hardware is test-and-set. This instruction allows the atomic testing and setting of a value.

The semantics of the instruction are below. Please remember that it is atomic. It is not interruptable.

test-and-set (variable) {
	temp = variable;
	variable = 1;
	return (temp);
}

This instruction can be used to obtain a lock for synchronization as below:

lock = 0;
entry:	while (test_and_set (lock));
exit:	lock = 0;

Unfortunately, this simple use of the instruction is inefficient -- it is a busy-wait. But worse -- it doesn't provide for a bounded wait.

Test-and-set Multi-Process Synchronization

The approach below allows for proper mutual exclusion among several processes or threads using test-and-set.

int waiting[N] = {false, ....}
int lock = false;

entry: 	waiting[i] = true;
	key = true;
	while (wating[i] && key) {
		key = test-and-set (lock));
	}
	waiting[i] = false;

exit:	j = (i+1)%N;
	while (j != i && waiting[j] == false)
		j = (j+1)%N;
	if (j == i) 
		lock = flase;
	else 
		waiting[j] = false;

Semaphores

Now that we have hardware support, we can build higher-level synchronization constructs that can make our life easier.

We invent a new type of variable, called a semaphore.It can be initially set to an integer value. After initialization, its value can only be affected by two operations:

P(x) was named from the Dutch word proberen, which means to test.
V(x) was named from the Dutch word verhogen, which means to increment.

Your book uses a more commen nomenclature. P(x) is referred to as wait. V(x) is referred to as signal. This is because the P() operation is usually used to wait to enter a critical section and the V() operation is usually used to signal the availability of the region (on exit).

Tannenbaum, a famous computer scientist, has his own nomenclature, he calls the P() operation, down and the V() operation up. This actually better defines what each operation does to its integer value.