Start Lecture #11

**Remark**: Don't forget that the labs are
**required**.
Currently 4 students have not yet submitted lab 1!

Now let's try being polite and really take turns. None of this wanting stuff.

Initially turn=1 Code for P1 Code for P2 Loop forever { Loop forever { while (turn = 2) {} while (turn = 1) {} critical-section critical-section turn <-- 2 turn <-- 1 non-critical-section } non-critical-section }

This one forces alternation, so is not general enough. Specifically, it does not satisfy condition three, which requires that no process in its non-critical section can stop another process from entering its critical section. With alternation, if one process is in its non-critical section (NCS) then the other can enter the CS once but not again.

The first example violated rule 4 (the whole system blocked). The second example violated rule 1 (both in the critical section. The third example violated rule 3 (one process in the NCS stopped another from entering its CS).

In fact, it took years (way back when) to find a correct solution.
Many earlier solutions

were found and several were published, but
all were wrong.
The first correct solution was found by a mathematician named Dekker,
who combined the ideas of turn and wants.
The basic idea is that you take turns when there is contention, but
when there is no contention, the requesting process can enter.
It is very clever, but I am skipping it (I cover it when I teach
distributed operating systems in V22.0480 or G22.2251).
Subsequently, algorithms with better fairness properties were found
(e.g., no task has to wait for another task to enter the CS twice).

What follows is Peterson's solution, which also combines
`wants` and `turn` to force alternation only when
there is contention.
When Peterson's algorithm was published, it was a surprise to see
such a simple solution.
In fact Peterson gave a solution for any number of processes.
A proof that the algorithm satisfies our properties (including a
strong fairness condition) for any number of processes can be found
in *Operating Systems Review* Jan 1990, pp. 18-22.

Initially P1wants=P2wants=false and turn=1 Code for P1 Code for P2 Loop forever { Loop forever { P1wants <-- true P2wants <-- true turn <-- 2 turn <-- 1 while (P2wants and turn=2) {} while (P1wants and turn=1) {} critical-section critical-section P1wants <-- false P2wants <-- false non-critical-section } non-critical-section }

Tanenbaum calls this instruction
test and set lock TSL

.

I call it test and set (TAS)

and define

TAS(b), where b is a binary variable,

to ATOMICALLY set b←true and return the OLD value of b.

Of course it would be silly to return the new value of b since we know the new value is true.

The word **atomically** means that the two actions
performed by TAS(x), testing (i.e., returning the old value of x)
and setting (i.e., assigning true to x) are inseparable.
Specifically it is **not** possible for two concurrent
TAS(x) operations to both return false (unless there is also another
concurrent statement that sets x to false).

With TAS available implementing a critical section for any number of processes is trivial.

loop forever { while (TAS(s)) {} ENTRY CS s<--false EXIT NCS }

**Remark:** Tanenbaum presents both busy waiting (as
above) and blocking (process switching) solutions.
We present only do busy waiting solutions, which are easier and used
in the blocking solutions.
Sleep and Wakeup are the simplest blocking primitives.
Sleep voluntarily blocks the process and wakeup unblocks a sleeping
process.
However, it is far from clear how sleep and wakeup are implemented.
Indeed, deep inside, they typically use TAS or some similar
primitive.
We will not cover these solutions.

**Homework:**
Explain the difference between busy waiting and blocking process
synchronization.

**Remark:** Tannenbaum use the term
semaphore *only* for blocking solutions.
I will use the term for our busy waiting solutions (as well as for
blocking solutions).
Others call our solutions *spin locks*.

The entry code is often called P and the exit code V. Thus the critical section problem is to write P and V so that

loop forever P critical-section V non-critical-sectionsatisfies

- Mutual exclusion.
- No speed assumptions.
- No blocking by processes in NCS.
- Forward progress (my weakened version of Tanenbaum's last condition).

Note that I use indenting carefully and hence do not need (and sometimes omit) the braces {} used in languages like C or java.

A **binary semaphore** abstracts the TAS solution we gave
for the critical section problem.

- A binary semaphore S takes on two possible values
open

andclosed

. - Two operations are supported
- P(S) is
while (S=closed) {} S<--closed -- This is NOT the body of the while

where finding S=open and setting S<--closed is*atomic* - That is, wait until the gate is open, then run through and atomically close the gate
- Said another way, it is not possible for two processes doing P(S) simultaneously to both see S=open (unless a V(S) is also simultaneous with both of them).
- V(S) is simply S<--open

The above code is *not* real, i.e., it is *not* an
implementation of P.
It requires a sequence of two instructions to be atomic and that is,
after all, what we are trying to implement in the first place.
The above code is, instead, a definition of the effect P is to have.

To repeat: for any number of processes, the critical section problem can be solved by

loop forever P(S) CS V(S) NCS

The only solution we have seen for an arbitrary number of processes is the one just above with P(S) implemented via test and set.

**Remark**: Peterson's solution requires each process to
know its process number; the TAS soluton does not.
Moreover the definition of P and V does not permit use of the
process number.
Thus, strictly speaking Peterson did not provide an implementation of
P and V.
He did solve the critical section problem.

To solve other coordination problems we want to extend binary semaphores.

- With binary semaphores, two consecutive Vs do not permit two subsequent Ps to succeed (the gate cannot be doubly opened).
- We might want to limit the number of processes in the section to 3 or 4, not always just 1.

Both of the shortcomings can be overcome by not restricting ourselves
to a *binary* variable, but instead define a
**generalized** or **counting** semaphore.

- A counting semaphore S takes on non-negative integer values
- Two operations are supported
- P(S) is
while (S=0) {} S--

where finding S>0 and decrementing S is*atomic* - That is, wait until the gate is open (positive), then run through and atomically close the gate one unit
- Another way to describe this atomicity is to say that it is not possible for the decrement to occur when S=0 and it is also not possible for two processes executing P(S) simultaneously to both see the same necessarily (positive) value of S unless a V(S) is also simultaneous.
- V(S) is simply S++

Counting semaphores can solve what I call the
*semi-critical-section problem*, where you premit up to k
processes in the section.
When k=1 we have the original critical-section problem.

initially S=k loop forever P(S) SCS -- semi-critical-section V(S) NCS

Note that my definition of semaphore is different from Tanenbaum's so it is not surprising that my solution is also different from his.

Unlike the previous problems of mutual exclusion, the producer-consumer has two classes of processes

- Producers, which produce times and insert them into a buffer.
- Consumers, which remove items and consume them.

What happens if the producer encounters a full buffer?

Answer: It waits for the buffer to become non-full.

What if the consumer encounters an empty buffer?

Answer: It waits for the buffer to become non-empty.

The producer-consumer problem is also called the
**bounded buffer** problem, which is another example of
active entities being replaced by a data structure when viewed at
a lower level (Finkel's level principle).

Initially e=k, f=0 (counting semaphores); b=open (binary semaphore) Producer Consumer loop forever loop forever produce-item P(f) P(e) P(b); take item from buf; V(b) P(b); add item to buf; V(b) V(e) V(f) consume-item

- k is the size of the buffer
- e represents the number of empty buffer slots
- f represents the number of full buffer slots
- We assume the buffer itself is only serially accessible. That is,
only one operation at a time.
- This explains the P(b) V(b) around buffer operations
- I use ; and put three statements on one line to suggest that a buffer insertion or removal is viewed as one atomic operation.
- Of course this writing style is only a convention, the enforcement of atomicity is done by the P/V.

- The P(e), V(f) motif is used to force
bounded alternation

. If k=1 it gives strict alternation.