Unit II — Process Synchronization · Critical Section · Semaphores · Classical Problems
Unit II

Process Synchronization — Race Conditions to the Sleeping Barber

Every Unit II synchronization question from the six 2023–2025 papers in OS Akash.pdf, plus the older-paper questions the recent book only cross-references (“Refer Q.5(a) End Term Exam 2019”) — where the recent book points elsewhere, the real content is rebuilt here from os-akash.pdf. Concepts come first, algorithms second, and no piece of pseudocode appears without an explanation of what it does.

How to read this pageSynchronization is the one Unit II block where the papers repeat themselves almost word for word. Learn the four ideas in sections A–D (race condition → critical section → the three requirements → mutual exclusion), and every algorithm in sections E–F becomes a special case of them. Sections G–K are the marks-earning classics: semaphores, busy waiting, Producer–Consumer, Dining Philosophers, Sleeping Barber.

A. Race Condition

Synchronization
Unit II · Race Condition

What is race condition? Illustrate it with example.

Recent PYQ — must do End Term Dec 2024 · Q.4(b) 2 Marks High
Asked in: End Term Dec-2024 Q.4(b) (2 marks) · Older papers: End Term Jul-2023 Q.1(d) (2.5) · End Term Jul-2016 Q.1(e) (marks: not clearly visible) · End Term May–June 2017 Q.4(b) (3) · End Term Jun-2019 Q.4(b) (3 — the two-task T1/T2 version over the shared integers X and Y, whose printed final answer is X := 0, Y := 1).
Show answer

Race condition. A race condition occurs when two or more processes or threads concurrently access the same shared data, at least one of them writes it, and the final value therefore depends on the particular order in which the concurrent accesses happen. The program is correct for some interleavings and wrong for others — and you cannot predict which interleaving the scheduler will pick.

The example as printed in the Dec-2024 answer

Two threads share one integer, and each simply adds one to it:

int counter = 0;

Thread 1:  counter = counter + 1;
Thread 2:  counter = counter + 1;

That one statement is three memory operations at machine level — read → modify → write — and the hardware may interleave them arbitrarily between the two threads. Using one register each:

Thread 1                  Thread 2
reg1 = counter;           reg2 = counter;
reg1 = reg1 + 1;          reg2 = reg2 + 1;
counter = reg1;           counter = reg2;
Interleaving that produces the wrong answer
OrderThread 1Thread 2counter in memory
1Reads counter (0)—0
2—Reads counter (0)0
3Adds 1 → 1—0
4—Adds 1 → 10
5Writes counter = 1—1
6—Writes counter = 11
Final value of counter1, not 2

Both threads read the same starting value 0, both computed 1, and the second write simply overwrote the first. One increment has been destroyed — which is why the symptom is called a lost update. Had the two threads run strictly one after the other the answer would be 2, so the same program gives two different results on two different runs.

Answer line to copy“Final value of counter is 1, not 2. This is a race condition because the result depends on the race between thread execution sequences.”

What a race condition needs before it can happen

  1. The data is shared — a global variable, a kernel table, a file, a device register, the buffer of a producer–consumer pair.
  2. At least one accessor writes it — read–read overlaps are harmless.
  3. The accesses overlap in time: two threads on two cores, or one thread preempted halfway through its update by another.

Remove any one of the three and the race disappears. In practice you cannot remove the sharing, so the OS makes the accesses mutually exclusive — which is the critical section of the next section.

MalfunctionWhat goes wrong
Lost updateTwo increments collapse into one, exactly as in the table above.
Inconsistent readA reader sees a structure half-updated — e.g. a linked list whose tail pointer was written before the new node’s link field.
Duplicated workTwo threads both test a flag, both see it clear, and both perform the one-shot job.

Where the same race reappears on this page: the two bit-flip tables of the Jul-2016 answer illustrate it, and it is the reason the 2016 paper Q.4(c) asks you to show that if wait and signal are not executed automatically, mutual exclusion may be violated — see Section G.

B. The Critical Section

Synchronization
Unit II · Critical Section

What do you mean by Critical Section? What are various methods to solve the critical section problem? Write a solution for Dining Philosophers problem using Semaphores.

Older PYQ — syllabus gap End Term May–June 2017 · Q.5(a) 8.5 Marks (whole question) High
Asked in: End Term May–June 2017 Q.5(a) (8.5 marks for the whole three-part question — its third part is answered in Section J · Dining Philosophers, its second part in sections D–F). The recent 2023–2025 papers never print this wording standalone, so this card teaches the definition and the four-part process structure that every recent answer silently assumes.
Show answer

Critical section. A critical section is that segment of code of a process in which the process accesses a shared object — it changes or reads a shared variable, updates a shared table, or writes to a shared file or device. Because simultaneous entry would corrupt that object, the rule is: while one process is executing in its critical section, no other process may execute in its critical section for the same object. “Critical” describes the section of code, not the shared variable.

The shared-variable example — Counter = 100

Let Counter be an integer shared by two processes, each of which must increment it once. The intended result of two increments is 102.

int Counter = 100;

P0:  Counter = Counter + 1;      P1:  Counter = Counter + 1;

Each statement again expands to read–add–write, and this time the two processes are allowed to interleave:

Interleaving of the two increments (Counter starts at 100)
OrderP0P1Counter
1Reads Counter = 100 into r0—100
2—Reads Counter = 100 into r1100
3r0 = r0 + 1 = 101—100
4—r1 = r1 + 1 = 101100
5Writes Counter = 101—101
6—Writes Counter = 101101
Final value101, not 102

P1’s write destroys P0’s increment, because P1 had already copied 100 into its own register before P0 stored anything. One increment is lost — so the final value of Counter can be 101 or 102 depending only on the order of execution. Controlling that order is exactly what a critical section is for.

Structure of a process that uses a critical section

Every process that touches the shared object is written in the same four-part shell. The entry and exit sections are the protocol that grants and releases permission; they are the only parts that may themselves contain races — which is why sections E and F spend so much effort on them.

do {
      entry section;        /* ask for permission to enter */
      /* CRITICAL SECTION */ /* the code that touches the shared object */
      exit section;         /* release permission */
      remainder section;    /* everything else — no shared data */
} while (true);
Fig B-1 · Structure of a process: entry → critical → exit → remainder, repeated forever
Process P i ENTRY SECTION spin / test the shared flag CRITICAL SECTION Counter = Counter + 1 EXIT SECTION release the flag REMAINDER SECTION no shared data used here do … while (true) Process P j — same object wants to enter its CS held in its entry section waiting / spinning, or blocked blocked Shared object Counter : 100 • only one process inside the red box • entry and exit are the protocol • remainder never needs the lock one at a time — OK The red box is where the race of section A can occur.
How to draw this in exam
  1. Draw one vertical column of four boxes: entry · critical · exit · remainder.
  2. Join them downward with arrows and wrap a long arrow from remainder back to entry, labelled do … while (true).
  3. Alongside, draw a second process whose arrow is stopped at its own entry box, labelled “blocked / waiting”.
  4. Shade or hatch only the critical-section box and write the shared variable next to it.
  5. Finish with the one-line rule: “no two processes may be in their critical sections at the same time”.

The four-part shell, term by term

SectionWhat it isMust it be short?
Entry sectionThe protocol code executed before entering: tests and sets the shared lock variable; may spin or may block.Yes — long entry sections mean long waiting for everyone else.
Critical sectionThe code that actually reads or writes the shared object (Counter = Counter + 1).Yes — holding it blocks all competitors.
Exit sectionReleases the lock so that another process can enter.Must execute — forgetting it deadlocks the system.
Remainder sectionAll remaining code of the process; touches no shared object under this protocol.No constraint.

“What are various methods to solve the critical section problem?”

That clause of the question expects the three-way classification, set out in full in Section D · Mutual exclusion and then implemented in Section E (software: Peterson’s, Dekker’s, Bakery’s) and Section F (hardware: disable interrupts, Test-and-Set, Swap). The third approach — programming-language constructs such as monitors and critical regions — is compared with semaphores in Section G.

C. The Critical-Section Problem and its Three Requirements

Synchronization
Unit II · Critical-Section Problem

Illustrate the Critical Section Problem? Write the requirements of its solution.

Recent PYQ — must do Mid Term Oct 2025 · Q.1(b) 2 Marks Very high
Asked in: Mid Term Oct-2025 Q.1(b) (2 marks) · Mid Term Oct-2024 Q.1(d) (2 marks — “Explain three requirements that a solution to critical-section problem must satisfy.”) · Mid Term Nov-2023 Q.3(a) (Marks: not clearly visible — “What is critical section problem? Explain three conditions that must satisfy to provide solution to critical section problem.”) · First Term Feb-2019 Q.1(e) (2 marks — “What are the requirements of any solution to the critical section problem ?”). The recent book prints only a cross-reference for three of these four: Oct-2024 and Nov-2023 both say “Refer to Q.5(a) of End Term Examination 2019 (Pg. no. 13–2019)”, Oct-2025 says “Refer Q.1(e) from First Term Feb. 2019 (Pg. 2–2019)”. The definitions below are the ones actually printed in those older answers.
Show answer

The critical-section problem. Suppose each of n processes has a section of code — its critical section — in which shared variables, shared tables or shared devices are changed. The critical-section problem is to design a protocol that the processes can use to cooperate, so that when one process is executing in its critical section no other process is allowed to execute in its critical section. The protocol is the entry section and the exit section that wrap the critical section:

do {
      entry section;
      /* critical section */   /* the shared Counter is written here */
      exit section;
      remainder section;
} while (true);

Any solution to the critical-section problem must satisfy three requirements.

Requirement 1 — Mutual Exclusion

If process Pi is executing in its critical section, then no other process may be executing in its critical section. This is the safety property: at most one process inside the critical region at any instant. It is violated by the naive “just check a flag” scheme, where two processes can both read the flag as clear and both charge in — which is the Counter = 101 outcome of Section B.

Requirement 2 — Progress

Printed wording from the older-paper answer: “If no process is executing in its critical section and there exist some processes that wish to enter their critical sections, then only those processes that are not executing in their remainder section can participate in the decision of which will enter its critical section next, and this decision cannot be postponed indefinitely.” Two halves to remember: (a) processes sitting quietly in their remainder section must not be able to influence the decision — otherwise a process that has no interest in the shared data blocks the one that does, which is the flaw of strict alternation; (b) if the lock is free, somebody who is waiting must get in. Progress is therefore also the “no deadlock” property.

Requirement 3 — Bounded Waiting

There must exist a bound on the number of times other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted. In other words a waiting process cannot be overtaken an unlimited number of times; the printed answer adds the practical picture — “once a process enters its critical section, it does not get another turn until a waiting process gets a turn (managed as a queue)”. The bound is normally stated as “at most n − 1 overtakes”. Bounded waiting is the anti-starvation property: the plain Test-and-Set loop satisfies the first two requirements but not this one, which is why the bounded-waiting variant exists (Section F).

Which requirement catches which failure
RequirementGuaranteesFailure it preventsBroken by
Mutual ExclusionAt most one process in the CS at a timeLost update / corrupted shared stateA non-atomic test-then-set in the entry section
ProgressA free lock goes to a requester, and idle (remainder) processes cannot vetoDeadlock; rigid turn-takingStrict alternation on a single turn variable
Bounded WaitingA finite cap on how often others may jump ahead of youStarvation / indefinite postponementwhile (testAndSet(&lock)) ; with no queue
Exam tipFor 2 marks, write the one-sentence statement of the problem plus the three named requirements with one line each, and underline the three names. For the 10-mark Nov-2023 version (Q.3 carried 10 marks for two parts) add the do { entry; CS; exit; remainder } while (true); block, the Counter = 100 → 101 illustration and Fig B-1; that turns three definitions into a complete answer.

Two further assumptions usually stated with them

  • Each process must exit its critical section only from within that critical section, and it must remain there for only a bounded time — otherwise “no other process can enter” becomes permanent.
  • No assumption is made about the number or the relative speeds of processes and CPUs. Once a process starts, it cannot be stopped except by another process.

D. Mutual Exclusion

Synchronization
Unit II · Mutual Exclusion

Define and implement the hardware solution to mutual exclusion problem.

Recent PYQ — must do End Term Jan 2024 · Q.4(b) 7.5 Marks Very high
Asked in: End Term Jan-2024 Q.4(b) (7.5 marks) — this card is its “define” half, and Section F is its “implement” half. The coverage matrix also lists End Term May–June 2017 Q.5(a) (8.5 marks) against this topic. The printed Jan-2024 answer covers the concepts only; the extraction notes record that the “implement” part — the Test-and-Set / Swap code — is not on those pages.
Show answer

Mutual exclusion is a property of process synchronization: no two processes can exist in the critical section at any given point of time. The term was first coined by Dijkstra. It is the enforcement half of the relationship — the critical section names the dangerous code, mutual exclusion names the rule that keeps that code single-file.

Why the need arises — with concurrency

Mutual exclusion is unnecessary in a strictly sequential system, because there is only one instruction stream and a read–modify–write sequence can never be interrupted halfway. The need appears the moment execution becomes concurrent: several instruction streams making progress over the same interval while sharing some object. Concurrency creates the interleaving of Section A, and the interleaving creates the lost update.

The four kinds of concurrent execution

#Kind of concurrencyWhy it can corrupt shared data
1Interrupt handlersAn interrupt can fire in the middle of a kernel data-structure update and run a handler that touches the same structure.
2Interleaved processes / threads — one CPU, preemptive schedulingThe clock interrupt preempts a process halfway through its read–modify–write and dispatches another that performs the same update.
3Multiprocessor / clustered systems with shared memoryTwo processes genuinely execute the same instruction at the same instant on different cores; no interrupt is needed to interleave them.
4Distributed systemsSeparate machines share a logical object (a file, a database row, a spool directory), with no common clock to order the accesses.

Three approaches to implementing mutual exclusion

ApproachHow it worksCost / weaknessWhere on this page
1. Software methodThe responsibility for enforcing exclusion is left to the processes themselves: they cooperate through shared variables written in ordinary code.Error-prone, and high overhead — the protocol is delicate and it busy-waits.Section E — Peterson, Dekker, Bakery
2. Hardware methodSpecial-purpose machine instructions (disable interrupts, Test-and-Set, Swap) make the whole test-and-set one atomic step.Faster, but gives no guarantee against deadlock or starvation.Section F
3. Programming-language methodThe compiler and runtime provide a construct — monitor, critical region, lock object — that wraps the shared data automatically.Needs language support; the programmer must know which data is shared.Section G

The six requirements of mutual exclusion — as printed

  1. Only one process may be in its critical section at a time.
  2. The mechanism must be implementable purely in software on a machine — it may not assume special hardware that the machine does not provide.
  3. A process must remain in its critical section for a bounded time only.
  4. No assumption may be made about the relative speeds of asynchronous concurrent processes, nor about the number of processors.
  5. A process that is outside its critical section cannot prevent another process from entering its critical section.
  6. A process must not be indefinitely postponed from entering its critical section.

Read requirements 1, 5 and 6 against Section C and the mapping is immediate: 1 = mutual exclusion, 5 and the bounded-time clauses = progress, 6 = bounded waiting. The other clauses (2, 3, 4) are what makes the requirement set implementable rather than merely idealistic — a scheme that needs a lock-free broadcast bus, or that assumes all processes run at the same speed, is not admissible.

How to score the 7.5 marksThe verb pair is “define and implement”. Structure the answer in four beats: (i) definition + Dijkstra, (ii) the need arising with concurrency and the four kinds listed, (iii) the three approaches with their merits, (iv) the implementation — the disable-interrupt, Test-and-Set and Swap code of Section F. The book’s printed answer stops at (iii), so (iv) is where the difference is made.

E. Software Solutions to the Mutual-Exclusion Problem

Algorithms
Unit II · Software Solutions

Software solution to Mutual exclusion Problem — the general single-variable and two-variable attempts, and why each one fails.

Syllabus only — no direct PYQ Syllabus line: “software solution to Mutual exclusion Problem” Medium
Show answer

This build-up is not asked as a question by itself in either book, but the Peterson’s and Bakery’s answers are impossible to justify without it: the marks are earned by saying which requirement a naive scheme breaks. Assume exactly two processes, P0 and P1; for Pi write j for the other process (j = 1 − i).

Attempt 1 — one shared variable: turn (strict alternation)

int turn = 0;                     /* shared */

/* P0 */  while (turn != 0) { /* spin */ }   /* entry */
          /* CRITICAL SECTION */
          turn = 1;                           /* exit */

/* P1 */  while (turn != 1) { /* spin */ }   /* entry */
          /* CRITICAL SECTION */
          turn = 0;                           /* exit */

Read it: one variable records whose go it is; each process spins until the turn equals its own number, and hands the turn over on exit. Only one process can ever be inside, so mutual exclusion holds. But suppose P0 finishes, sets turn = 1, and then never wants the critical section again, while P1 wants it twice in a row. P1 completes one pass, sets turn = 0, and on its second request spins forever although P0 is idle — it is being vetoed by a process that is not even competing. Progress fails.

Attempt 2 — one shared variable: flag (“is anybody inside?”)

boolean flag = false;             /* shared */

while (flag) { /* spin */ }       /* entry: is somebody inside? */
flag = true;
/* CRITICAL SECTION */
flag = false;                     /* exit */

Read it: a single boolean says whether someone is inside; look before you leap, then raise the flag. The fatal detail is that look and leap are two separate memory operations, so the test-and-set is not atomic: P0 reads flag == false and is preempted, P1 also reads flag == false, and both then set it — and both enter. Mutual exclusion fails, for exactly the reason Counter became 101. Any software solution must therefore get atomicity from somewhere: the standard trick is to require only that a single memory write be atomic, and to spread the state over several variables so that no one test-then-set pair can be broken in half.

Attempt 3 — two shared variables: flag[2] (interest only)

boolean flag[2] = {false, false};   /* shared: flag[i] = "Pi wants in" */

flag[i] = true;
while (flag[j]) { /* spin */ }      /* wait while the other is interested */
/* CRITICAL SECTION */
flag[i] = false;

Read it: each process announces its interest by writing its own variable — a write that is atomic — and waits while the other’s flag is set. Mutual exclusion now holds: if both are interested, neither can pass its loop. But both may set their flags before either checks: P0 sets flag[0], P1 sets flag[1], each then sees the other’s flag set, and both spin politely forever at a lock that is in fact free. Progress fails — a deadlock created purely by the protocol.

Attempt 4 — two shared variables: flag[2] + turn, naively combined

/* P0 */  flag[0] = true;  while (flag[1]) { /* spin */ }  turn = 0;
/* P1 */  flag[1] = true;  while (flag[0]) { /* spin */ }  turn = 1;

Read it: raise your flag, wait for the other to lower its, and only then record the turn. Because the flag is raised before the test, the two processes can again both be stuck waiting on each other; and if the interleaving lets one process pass the test just before the other’s flag store lands, both enter. So this version can break both mutual exclusion and progress. The repair is a change of order: set turn after raising your own flag, so that whoever wrote last is the one that waits. That is Peterson’s algorithm.

The ladder of failed attempts — this table is the introduction to every algorithm answer below
AttemptShared variablesMutual exclusionProgressBounded waitingFatal defect
1 Strict alternationturnYesNoYesA process in its remainder section still holds a veto.
2 Single flagflagNoYesYesTest and set are two separate steps — not atomic.
3 Interest flagsflag[2]YesNoYesSymmetric deadlock when both are interested.
4 flag + turn (naive)flag[2], turnNoNoNoWrong order — turn written only after the waiting.
5 Peterson’sflag[2], turnYesYesYesTwo processes only; assumes ordered memory.
Unit II · Software Solutions

Peterson’s algorithm — the software solution asked inside “What are various methods to solve the critical section problem?”

Older PYQ — syllabus gap End Term May–June 2017 · Q.5(a) 8.5 Marks (whole question) Medium
Asked in: End Term May–June 2017 Q.5(a) (8.5 marks) — the printed answer devotes several pages to Peterson’s algorithm, including the pseudocode and three worked “EXAMPLE” step tables. Peterson’s is not asked by name in any 2023–2025 paper; the coverage matrix lists it under Tier 3 (“syllabus-listed but NOT asked in the recent papers”) and under row 28 as “Peterson + Dekker 2017”.
Show answer

Purpose

To achieve mutual exclusion between exactly two processes using only two shared variables and ordinary atomic reads and writes — no special machine instruction at all. It repairs attempt 4 above purely by ordering the writes: each process first declares its interest, then politely gives the turn away, and only then checks.

The variables

VariableTypeInitial valueMeaning
flag[2]array of booleanfalse, falseflag[i] = true — “Pi wants to enter its critical section”.
turninteger, 0 or 10Which process may go next — and it is always set to the other process’s number.

Pseudocode

boolean flag[2];      /* both false */
int     turn;         /* 0 or 1     */

do {
      flag[i] = true;                        /* 1. I am interested        */
      turn      = j;                         /* 2. but you may go first   */
      while (flag[j] && turn == j) { /* spin */ }  /* 3. wait if both hold */

      /* CRITICAL SECTION */

      flag[i] = false;                       /* 4. no longer interested   */

      /* REMAINDER SECTION */
} while (true);

Read the entry section as one sentence: “I want in, and I concede priority to you; I will hang around only while you actually want in and it really is your turn.” Statement 3 is a spin loop — Peterson’s is a busy-waiting solution.

Why it works — plain-English walkthrough

  1. Mutual exclusion. Suppose both were inside. For Pi to have passed its loop, either flag[j] was false — impossible while Pj is inside, because Pj raises its flag before entering and lowers it only on exit — or turn != j. By symmetry Pj needs turn != i. But turn holds exactly one of the two values, so both cannot hold: at most one process passes.
  2. Progress. If both are interested, both execute their turn = j statements and the last write wins; so exactly one process sees turn == j and waits, while the other sees a false condition and enters at once. A process in its remainder section has flag == false, which alone is enough to release the loop — idle processes take no part in the decision.
  3. Bounded waiting. The one that loses the race over turn gets in once the winner eventually executes flag[i] = false on exit; the loser then sees the flag clear and enters. On a single processor that is all it takes, because the winner is the only other process that can enter, so the loser is overtaken at most once per request. (With more than one CPU a third process could in principle intervene, which is one more reason the algorithm is stated for two processes.)
  4. The worked interleaving (this is the “EXAMPLE” table the 2017 answer draws). P0 executes steps 1 and 2 → flag[0]=true, turn=1. P1 now executes 1 and 2 → flag[1]=true, turn=0. P0 tests flag[1] && turn==1 = true && false → it proceeds into its critical section. P1 tests flag[0] && turn==0 = true && true → it waits. The process that wrote turn last is the one that waits.

Limitations

  • Two processes only. The concession turn = j has no natural meaning for n > 2; generalising it to n processes is what Bakery’s algorithm does with ticket numbers (next card).
  • It busy-waits — the loser burns CPU in the while loop (see Section H).
  • It assumes ordered (sequentially consistent) memory. On real multiprocessors the CPU and compiler may reorder the flag store after the turn store; without a memory barrier both processes can read stale flags and both enter. Peterson’s is correct as a textbook algorithm, not as kernel code.
Unit II · Software Solutions

Discuss Dekker's Algorithm.

Older PYQ — syllabus gap 2017 paper · Q.5(b) 4 Marks Medium
Asked in: Q.5(b) of the older book’s 2017 section (book page 27, “Discuss Dekker's Algorithm.”, 4 marks). The extraction notes record that the examination banner for that page lies outside the photographed range, so which 2017 paper it is cannot be confirmed. Dekker’s is not asked anywhere in the 2023–2025 papers (coverage matrix, Tier 3: “Peterson’s & Dekker’s”).
Show answer

Purpose — and the book’s own opening line

Printed verbatim in the older paper: “Dekker's algorithm was the first provably-correct solution to the critical section problem. It requires both an array of boolean values and an integer variable.” Like Peterson’s it solves the two-process case purely in software. Its difference is the way it breaks symmetry: instead of the last writer simply losing, a process that finds the turn held by the other backs off entirely, waits, and then asks again.

The variables

VariableTypeInitialMeaning
flag[0..1]array of booleanfalse, false“Pi wants to enter (or is inside)”.
turninteger, 0..10Who has the right of way. The process named by turn is the one that must be allowed through — so the other must back off.

Pseudocode — as printed in the older answer

var flag : array [0..1] of boolean;   /* both false */
    turn : 0..1;                      /* say, 0     */

repeat
      flag[i] := true;                       /* announce interest */
      while flag[j] do                       /* the other is interested too */
            if turn = j then                 /* ... and the right of way is his */
            begin
                  flag[i] := false;          /* 1. I back off completely      */
                  while turn = j do no-op;   /* 2. I wait while he is inside  */
                  flag[i] := true;           /* 3. now I ask again            */
            end;
      /* CRITICAL SECTION */
      turn := j;                             /* hand the right of way over */
      flag[i] := false;                      /* leave                      */
      /* REMAINDER SECTION */
until false;

The three statements inside begin … end are the heart of Dekker’s. If the turn is the other’s, you must not push on: you lower your own flag so the other process can finish testing, spin until it has left (that is, until turn comes back to you), and then raise your flag again and re-enter the outer test.

Why it works — plain-English walkthrough

  1. Both processes raise their flags, so the outer test while flag[j] is true for both and both fall into the “someone else is interested” branch.
  2. Exactly one of them is named by turn. Suppose turn = 1. For P0 the inner test turn = j reads turn = 1 and is true, so P0 lowers its flag and waits at while turn = j do no-op. For P1 the same test reads turn = 0 and is false, so P1 does nothing and re-tests — against P0’s flag, which has just fallen. P1 enters, and P0 cannot pass its inner wait until P1 leaves. That is mutual exclusion.
  3. P1 exits by executing turn := j — i.e. turn := 0 — and then flag[1] := false. That release frees P0 from its inner spin; P0 raises its flag again, sees flag[1] = false, and enters. So an idle lock never stays idle: progress holds.
  4. A process that backs off is guaranteed the turn next, so it cannot be jumped over more than once: bounded waiting holds.

Limitations

  • Two processes only — like Peterson’s, it does not scale to n; use Bakery for n.
  • Notoriously harder to read and to prove than Peterson’s: it nests two spin loops and uses a “back off and retry” structure, so it is easy to get wrong in an exam.
  • Both loops are busy-waits — spinning no-op keeps a CPU (or a core) from doing useful work.
  • Correctness assumes atomic, ordered reads and writes of flag and turn, so it is unsafe on out-of-order multiprocessors without memory barriers.
Unit II · Software Solutions

Explain bakery Algorithm. Prove that it satisfy all the three requirements for critical section problem.

Recent PYQ — must do Mid Term Oct 2024 · Q.4(a) 5 Marks High
Asked in: Mid Term Oct-2024 Q.4(a) (5 marks) — but the recent book only cross-references the older paper: the printed answer is literally “Refer Q5 (a) End Term Exam 2019 (Pg. no. 13–2019)” and contains no algorithm. The coverage matrix records the same fact (“Oct-2024 Q.4(a) Bakery (asked; answer cross-refs 2019)”). The identical wording is recorded again for End Term Dec-2024 (5 marks), also as a bare cross-reference. The real content below is rebuilt from the older papers: End Term Jun-2019 Q.5(a) (6 marks — “Show how the Bakery algorithm satisfies the requirements of a mechanism to control access to a critical section.”, book pp. 13–14) · End Term Jul-2016 Q.4(a) (Marks: not clearly visible) · End Term Jul-2023 Q.5(b) (6.5 marks — the question number is ambiguous on the blurred page and reads 5(b) or 6(b)) · Q.4(a) of the 2017 section (4 marks — page attribution ambiguous between Feb-2017 and May–June-2017, and its printed answer is itself only “Refer Q4(a) First Term Examination 2016”).
Show answer

Purpose

Printed opening of the older answer (with the book’s own typos, as the notes record them): “The Bakery algorithm is one of the simplest known solutions to the mutual exclusion problem for the general case of N processes.” Peterson’s and Dekker’s are two-process protocols; Bakery is the software solution that works for an arbitrary number of processes, and it does so without any special machine instruction.

The idea, in the book’s own words

Quoting the printed answer: “The basic idea is that each non-thinking process has a variable that indicates the position of that process in a hypothetical queue of all the non-thinking processes. Each process in this queue scans the variables of the other processes, and enters the critical section only upon determining that it is at the head of the queue.” Lamport’s metaphor is the counter of a bakery: you take a numbered token before you are served, and the clerk always serves the smallest number on display. The notes also record that the printed answer then admits “the resulting algorithm is still not easy to understand” and presents a simplified version first — worth saying in an exam, because it shows you know the hard part.

Fig E-1 · Bakery: each process takes a ticket at the counter; the smallest (number, process-id) pair is served
Ticket counter number[k] = 1 + max(number[ ]) choosing[k] = true choosing[k] = false queue of non-thinking processes, scanned as tuples (number[i], i) P0(3, 0) P1(1, 1) P2(3, 2) P3(2, 3) ignore a ticket while choosing[k] is true CRITICAL SECTION P0, P2, P3 still waiting number[1] = 0 on exit P1 scans every other ticket; the smallest tuple is served first. smallest tuple enters Ties on number are broken by the process id, so the order is total.
How to draw this in exam
  1. Left: one box “ticket counter”, annotated with number[k] = 1 + max(number[]).
  2. Right: a row of process boxes, each labelled with its tuple (number, id).
  3. Circle the smallest tuple and arrow it into a shaded “critical section” box.
  4. Add one dashed line from the counter into the queue to show the scan, and write “ties broken by id”.

The variables

VariableType / rangeInitialMeaning
boolean choosing[n]array of booleanfalsechoosing[i] = true — Pi is in the middle of picking its number, so its ticket is not yet trustworthy and others must wait.
int number[n]array of non-negative integers0The ticket. number[i] = 0 means “not interested”. Chosen as one more than the largest number on display.

Two shorthands used below: (number[i], i) < (number[k], k) means the pair is compared lexicographically — either number[i] < number[k], or the two numbers are equal and i < k. And max(a₀ … aₙ₋₁) returns the largest value, breaking ties by the largest index.

Pseudocode

do {
      choosing[i] = true;                       /* my ticket is not final yet   */
      number[i]   = max(number[0..n-1]) + 1;     /* take the next token          */
      choosing[i] = false;                      /* ticket is now stable         */

      for (k = 0; k < n; k++)
            if (k != i) {
                  while (choosing[k]) ;                                   /* a */
                  while (number[k] != 0 &&
                         (number[k], k) < (number[i], i)) ;                /* b */
            }

      /* CRITICAL SECTION */

      number[i] = 0;                            /* surrender the token          */

      /* REMAINDER SECTION */
} while (true);

Read it line by line. (1) Declare that a number is being picked, so nobody judges you on a half-written value. (2) Take a ticket one greater than the highest ticket visible. (3) Mark the ticket stable. (4) Then walk through every other process: at (a) wait until its ticket is stable, at (b) wait while it holds a non-zero ticket that sorts ahead of yours. Only when the whole scan finishes with nobody ahead of you do you enter. (5) On exit you set your number to 0 — that is how “I am not interested” is written.

Proof of the three requirements — the two-case argument

(1) Mutual exclusion. Suppose Pi is in its critical section and Pk (k ≠ i) has already chosen number[k]. There are two cases:

  1. Case 1 — (number[i], i) < (number[k], k). When Pk’s scan reaches index i it finds number[i] != 0 and the tuple comparison true, so Pk waits in loop (b). It can leave that loop only when Pi sets number[i] = 0, i.e. only after Pi has left its critical section. So the two are never inside together.
  2. Case 2 — (number[k], k) < (number[i], i). Then Pk chose its ticket before Pi did: had Pi chosen later, its max(number[0..n-1]) + 1 would have seen number[k] and returned something strictly larger, contradicting (number[k],k) < (number[i],i). But Pi is inside, so its scan must already have passed index k — impossible while Pk held the smaller non-zero ticket. The only consistent reading is that Pk had not yet reached its own scan when Pi passed, and having now reached it Pk waits behind Pi. Mutual exclusion holds in both cases.

Because the comparison is lexicographic, one of the two cases always holds: ties on number are broken by the process id, so the hypothetical queue has a total order and two processes can never occupy the same position and fight over it.

(2) Progress. Only processes with number[k] != 0 — those that actually asked to enter — are ever considered by the scan, so a process sitting in its remainder section holds 0 and cannot veto anybody. Among the requesters, the scan takes the smallest (number, id) pair, and that choice is fixed by values already written; the decision is reached in finite time and cannot be postponed indefinitely.

(3) Bounded waiting. Once Pi holds (t, i), every process that arrives later takes max(...) + 1 > t and therefore sorts behind Pi — it cannot overtake. Only the processes already holding smaller tickets may go first, and there are at most n − 1 of them, each getting at most one turn before Pi’s ticket reaches the head. So Pi waits for at most n − 1 entries: a bound exists, hence no starvation.

Limitations

  • Busy waiting, twice over — a process spins in loops (a) and (b), so n processes can keep n − 1 CPUs spinning for nothing.
  • O(n) shared-memory reads per entry; the scan, not the lock, is the cost, and on exit a holder must still be waited for by everybody else.
  • Ticket numbers grow without bound in this form, and choosing[] is essential: without it a torn read of a half-written number[k] can hand two processes the “same” position and break mutual exclusion.
  • It still needs atomic single-word reads and writes; on weakly-ordered hardware the scan must be fenced.

F. Hardware Solutions to the Mutual-Exclusion Problem

Algorithms
Unit II · Hardware Solutions

Define and implement the hardware solution to mutual exclusion problem.

Recent PYQ — must do End Term Jan 2024 · Q.4(b) 7.5 Marks Very high
Asked in: End Term Jan-2024 Q.4(b) (7.5 marks) — the “define” half of that question is answered in Section D; this card is its “implement” half. Swap is separately asked inside End Term May–June 2017 Q.5(a) (8.5 marks — see the Swap card).
Show answer
Source honesty — read before you write this answer Test-and-Set has no direct PYQ in either book. The extraction notes say so three times over: “Test-and-Set — NOT FOUND”, “Test-and-Set / Swap — NOT FOUND” for the older range, and for the recent 7.5-mark paper, “this answer covers concepts but the ‘implement’ part (Test-and-Set / Swap code) is not on these pages”. The coverage matrix is equally blunt: row 29, “Swap only … Test-and-Set never asked”. It is nevertheless written out in full here because “hardware solution to mutual exclusion” is itself a recent 7.5-mark question (Jan-2024 Q.4(b)) whose verb is implement. Treat the code as the expected implementation, not as something the notes print.

What makes a solution “hardware”

Every software attempt in Section E had to build atomicity out of pieces that are not atomic. Hardware solutions invert that: the machine supplies an instruction that performs test and set (or read and write) in one uninterruptible step. The entry section then collapses into a single call to such an instruction, and mutual exclusion follows from the instruction’s atomicity rather than from a clever ordering of writes.

1. Disable interrupts

while (true) {
      disable_interrupts();      /* entry — no interrupt can arrive now */
      /* CRITICAL SECTION */
      enable_interrupts();       /* exit  — interrupts allowed again    */
      /* REMAINDER SECTION */
}

Why it works: on a uniprocessor the only way to switch from one process to another is an interrupt — the clock tick, or an I/O completion. With interrupts off, the running process cannot be preempted, so no other process can reach the shared data while its critical section executes. Mutual exclusion is therefore absolute.

Why it is a bad idea in general: disable_interrupts() is a privileged instruction, so user code must not be allowed to switch off the clock — a process could then monopolise the machine and defeat the scheduler. It does nothing at all on a multiprocessor: masking interrupts on CPU 1 does not stop CPU 2 from touching the same memory. And it slows the whole system, because the interrupt latency of unrelated devices grows while the critical section runs. It survives as the standard technique for very short kernel-mode critical regions, which is exactly the “interrupt handlers” row of the concurrency table in Section D.

2. Test-and-Set

boolean testAndSet(boolean *target) {
      boolean oldValue = *target;
      *target = true;
      return oldValue;           /* the read AND the write are ATOMIC     */
}

Read it: the instruction stamps true into the target and hands back the value that was there before, and nothing can come between the read and the write. Two processes calling it simultaneously get different answers: exactly one receives false — the lock was free and it has just taken it — and every other caller receives true and must keep trying.

boolean lock = false;                   /* shared, one lock per critical region */

do {
      while (testAndSet(&lock)) ;   /* entry: spin until a call returns false   */
      /* CRITICAL SECTION */
      lock = false;                 /* exit:  a plain release                   */
      /* REMAINDER SECTION */
} while (true);

Walkthrough: P0 calls testAndSet, receives false, so the while body never runs and P0 enters; lock is now true. P1 calls it, receives true, and therefore re-calls it in the loop, receiving true every time — because each failed call re-stamps lock as true. When P0 finishes and stores lock = false, P1’s next call returns false and it enters. Mutual exclusion: satisfied. Progress: satisfied — a free lock is taken by whichever caller next executes the instruction.

Bounded waiting: NOT satisfied. Nothing orders the losers. A process that has just left its remainder section can win the race against a process that has been spinning for minutes, and on a multiprocessor the spinner may keep losing simply because the other’s stores land closer in time. That is starvation, so the plain loop needs a queue.

3. The bounded-waiting Test-and-Set variant

The repair is to record who is waiting in a shared array and, on exit, to open the door for the next waiter in cyclic order instead of for whoever happens to win the race.

boolean lock = false;                  /* the test-and-set lock   */
boolean waiting[n];                    /* all false initially     */
int     n;                             /* number of processes     */

do {
      waiting[i] = true;                        /* 1. raise my hand          */
      key = true;
      while (waiting[i] && key)                 /* 2. queue + lock race      */
            key = testAndSet(&lock);
      waiting[i] = false;                       /* 3. I am through           */

      /* CRITICAL SECTION */

      j = (i + 1) % n;                          /* 4. find the next waiter   */
      while ((j != i) && !waiting[j])
            j = (j + 1) % n;                    /*    ... cyclically         */

      if (j == i)  lock = false;                /*    nobody: release lock   */
      else         waiting[j] = false;          /*    somebody: hand it over */

      /* REMAINDER SECTION */
} while (true);

Walkthrough. Step 1 makes the request visible to everybody. Step 2 spins only while both “I am still queued” and “I have not yet got the lock” hold — the second half of that condition is what lets an exiting process drag a waiter out by clearing its waiting entry instead of releasing the lock. Step 4 is the whole point: on leaving, the process scans waiting[] forward from its own index, wrapping at n. If it finds a waiter it does not touch lock — it clears that waiter’s waiting[j], which breaks the waiter out of its loop and passes the lock directly to it. Only when the scan comes full circle with nobody waiting is lock set to false and returned to the pool. Because service moves in one fixed direction around the ring, no process can be overtaken more than n − 1 times: mutual exclusion, progress and bounded waiting all hold. The price is still spinning, plus an O(n) scan executed while holding the lock.

The hardware mechanisms side by side
MechanismAtomic primitiveMutual excl.ProgressBounded waitingMain problem
Disable interruptsInterrupt maskingYesYesYes (single CPU)Privileged only; useless on a multiprocessor; hurts responsiveness.
Test-and-Set (simple loop)testAndSet(&lock)YesYesNo — starvationBusy waits; hammers the memory bus; one lock per region.
Test-and-Set + waiting[]Same + shared arrayYesYesYes (≤ n − 1)Busy waits; O(n) scan inside the exit section.
Swapswap(&lock, &var)YesYesNoBusy waits; every spin costs a memory write — see next card.
Exam tipThe order that mirrors the marks: (1) definition + Dijkstra + the four kinds of concurrency, (2) the three approaches, (3) the six requirements — all from Section D — then (4) the implementation: disable-interrupt code, the Test-and-Set definition and its entry/exit use, the starvation flaw of the plain loop, and the bounded-waiting variant. Close with the table above. Examiners look for one particular sentence: “Test-and-Set guarantees mutual exclusion but not bounded waiting.”
Unit II · Hardware Solutions

Hardware solution using the Swap (exchange) instruction — from “What are various methods to solve the critical section problem?”

Older PYQ — syllabus gap End Term May–June 2017 · Q.5(a) 8.5 Marks (whole question) High
Asked in: End Term May–June 2017 Q.5(a) (8.5 marks) — the printed answer gives the Swap instruction with the loop while (var == true) swap(lock, var);. This is the only hardware instruction named in any of the papers; the coverage matrix row 29 records it as “Swap only (May–June 2017 Q.5(a)); Test-and-Set never asked”.
Show answer

Purpose

Swap (also called exchange) is the second standard atomic instruction. Where Test-and-Set writes the constant true and returns the old value, Swap exchanges the contents of two boolean variables in one uninterruptible step. That extra flexibility lets a process both ask for the lock and remember what it got, in a single memory transaction.

The instruction and the variables

void swap(boolean *a, boolean *b) {   /* executes ATOMICALLY */
      boolean temp = *a;
      *a = *b;
      *b = temp;
}
VariableScopeInitialMeaning
lockshared booleanfalse“Someone holds the region”.
var (also called key)local to each processtrueThe other half of the exchange — it receives whatever lock held.

Pseudocode — including the line exactly as printed in the 2017 answer

do {
      var = true;
      while (var == true)                 /* printed verbatim: while (var == true) swap(lock, var); */
            swap(&lock, &var);
      /* CRITICAL SECTION */
      lock = false;                       /* exit */
      /* REMAINDER SECTION */
} while (true);

Read it. The process seeds its local var with true and keeps exchanging it with lock. After one swap, lock holds true — so the next caller will fail too — and var holds whatever lock held beforehand. The loop ends only when the value received is false, i.e. only when this process found the region free and has just marked it busy. Exiting is the plain store lock = false.

Why it works, and what it costs

  • Mutual exclusion: exactly one caller can receive false from the exchange, because the swap is atomic — the winner writes true into lock in that same step, so a simultaneous loser necessarily reads back true.
  • Progress: when the holder stores lock = false, the next spinning swap returns false and that process enters.
  • Bounded waiting: no. As with the plain Test-and-Set loop, winners are chosen by the memory bus rather than by a queue, so an overtaken process can starve; the waiting[] construction of the previous card repairs it in the same way.
  • Cost: every failed spin still performs a memory write (it pushes true back into lock), so on a shared bus the loop generates far more traffic than a read-only spin would.
Test-and-Set vs Swap — the differences examiners expect
BasisTest-and-SetSwap
Atomic operationWrites a constant true, returns the old valueExchanges the values of two boolean variables
ArgumentsOne pointer (the target)Two pointers (shared + local)
Local variable needed?No — the return value servesYes — var / key, seeded with true
Entry testwhile (testAndSet(&lock)) ;while (var == true) swap(&lock, &var);
Bus traffic while spinningOne write per attemptA two-way exchange per attempt
Mutual exclusionGuaranteedGuaranteed
Bounded waiting (plain loop)Not guaranteedNot guaranteed
Asked in your papersNever asked by nameInside May–June 2017 Q.5(a)

G. Semaphores

Synchronization
Unit II · Semaphores

Define Semaphores. What are various types of semaphores? How are they different from critical regions?

Recent PYQ — must do End Term Dec 2024 · Q.4(c) 5.5 Marks Very high
Asked in: End Term Dec-2024 Q.4(c) (5.5 marks) — definition, types and the semaphore-vs-critical-region difference are all printed there, including an Aspect | Semaphore | Critical Region table. Related recent papers: Mid Term Oct-2024 Q.4(c) and Mid Term Nov-2023 Q.1(e) (see Section H). Older papers: End Term May–June 2018 Q.1(d) semaphores vs monitors (2.5) and Q.5(b) counting from binary (6); and Q.4(c) of the 2016 section (Marks: not clearly visible) — “Show that if the wait and signal operation are not executed automatically (in process synchronization), then mutual exclusive may be violated.”
Show answer

Definition. A semaphore S is an integer variable that, apart from initialisation, can be accessed only through two standard atomic operations, wait() and signal() — Dijkstra’s original names P() and V(). It is a synchronisation tool the OS uses to control concurrent access to a shared resource: the integer counts how many entries are still permitted, and a process that finds the count exhausted is kept out — and, in the correct implementation, put to sleep.

wait(S)   /* P(S) */  { /* test-and-decrement: MUST be atomic */ }
signal(S) /* V(S) */  { /* increment                             */ }

The semantics in words: wait(S) blocks until S > 0 and then decrements S by one — “spend one permit”. signal(S) increments S by one — “return one permit”. Only initialisation touches S directly; no other code may read or write it.

Implementation 1 — the busy-wait version (as printed in your papers)

wait(S)   {  while (S <= 0) ;   /* spin, doing nothing */   S--;   }
signal(S) {  S++;   }

Read it. wait refuses to proceed while the count is zero or negative: it sits in an empty while loop re-testing S, and only when S is positive does it decrement and fall through. signal simply adds one, which releases the next waiting process. This version is the one printed for Nov-2023 Q.1(e) — but the spinning is precisely busy waiting, and the whole while (S<=0); S--; sequence is not atomic unless something makes it so: two processes can both see S = 1 and both decrement, driving S to −1 and letting both enter.

Verification noteThe Dec-2024 answer prints the busy-wait implementation as wait(S){ while(S<=0) S=S-1; } with signal(S){ S=S+1; }. As written, the decrement sits inside the loop body, so once S reaches 0 the loop keeps subtracting and S runs unboundedly negative — the process never leaves the loop and never enters. The correct busy-wait form is the one printed for Nov-2023 Q.1(e), while (S <= 0) ; S--; — loop first, decrement after. Learn the second form; if you reproduce the first, say what it should be.

Why atomicity of wait/signal is the whole answer — the 2016 proof

The 2016 paper asks you to show that if wait and signal are not executed automatically, mutual exclusion may be violated. Let S = 1 guard one critical region, and suppose wait is split into “test” and “decrement”. P0 tests (S > 0, true) and is preempted; P1 now tests (still true), decrements to 0 and enters; P0 resumes and completes only its decrement (S = −1) and also enters. Both are inside — mutual exclusion broken — and S has silently gone negative. The fix is exactly what the blocking implementation below does: make test-and-decrement one indivisible action, by disabling interrupts around it in the kernel or by using an atomic hardware instruction.

Types of semaphores

Various types of semaphores — as printed in the Dec-2024 answer
BasisCounting semaphoreBinary semaphore (mutex)
Range of valuesAny non-negative integerOnly 0 or 1
ManagesMultiple identical instances of a resourceA single shared resource / critical section
Typical initial valueK = number of instances1 (free)
Meaning of S = 0All K instances are in useThe resource is held by somebody
Enforces mutual exclusion alone?No — with K > 1, K processes enter at onceYes — that is its whole purpose
Example10 printers, 8 tape drives, n buffer slotsOne linked list, one shared counter
OperationsIdentical in both: wait()/P() and signal()/V()

Implementation 2 — the correct blocking implementation (no busy waiting)

To stop the spinning, the semaphore is made a record holding the integer plus a queue of blocked processes, and both operations are executed with interrupts disabled so that they are atomic:

typedef struct {
      int              value;
      struct process  *list;      /* wait queue of blocked PCBs */
} semaphore;

wait(semaphore *S) {
      S->value--;
      if (S->value < 0) {                  /* no permit was left      */
            add this process to S->list;    /* park it on the queue    */
            block();                        /* give up the CPU         */
      }
}

signal(semaphore *S) {
      S->value++;
      if (S->value <= 0) {                 /* somebody is parked      */
            remove a process P from S->list;
            wakeup(P);                      /* put P back on ready queue */
      }
}

Read it carefully — the sign convention is the trick. wait decrements first. If the result is non-negative, a permit existed and the process just continues — no queueing, no sleep. If the result is negative, this process has taken a permit that belonged to somebody else, so it adds itself to S->list and calls block(), which hands the CPU to the scheduler. signal likewise increments first; a result ≤ 0 means a permit that was owed has now come back, so it removes one PCB from the queue and wakeup()s it into the ready queue. A negative value is therefore exactly the number of blocked processes: value = −3 means three processes are parked on this semaphore’s queue.

The queue’s discipline is what buys bounded waiting: a FIFO wait queue bounds how long any process can be held. And because block()/wakeup() manipulate kernel lists, they too must be protected against races — hence “with interrupts disabled”. This is the sleep-lock construction the OS actually uses, and it is the version relied on by the producer–consumer and dining-philosopher solutions later on this page.

Fig G-1 · Blocking semaphore: value = −2 means two processes are parked on the semaphore’s own wait queue
CRITICAL SECTION P0 holds the permit on exit: signal(S) semaphore S value = −2 negative ⇒ two blocked wait / signal are atomic (interrupts off inside) got it wait queue S → list P1 P2 both called wait(S) → block() FIFO order ⇒ bounded waiting parked READY QUEUE P1 woken — resumes just after wait(S) signal(S): value++ then wakeup(head) P0 leaves the CS and returns the permit value < 0 ⇒ blocked Blocked processes join the ready queue — they never spin.
How to draw this in exam
  1. Three columns: the critical section, the semaphore record, the wait queue.
  2. Write value = −2 in the record and state aloud that the magnitude is the number of blocked processes.
  3. Draw a solid arrow “wait(S) → park” into the queue and a dashed arrow “signal(S) → wakeup” out of the queue into the ready queue.
  4. Label the ready queue and add the note “FIFO order ⇒ bounded waiting”.

Semaphore vs critical region

A critical region (also called a critical section construct, or a critical statement) is a language-level mechanism: the shared variable is declared private to a type, and every statement that modifies it is textually wrapped in a critical x { … } block, so the compiler guarantees that at most one process executes any region protecting that same variable. The Dec-2024 answer illustrates it with exactly this fragment — the guarded statement being a bank update:

/* critical section */
balance = balance + 100;

In semaphore style the same update must be bracketed by hand with wait(mutex); … signal(mutex); — and forgetting any one of those calls is a bug.

Semaphore vs Critical Region — the five aspects printed in the Dec-2024 answer
AspectSemaphoreCritical region
DefinitionAn integer variable accessed only through the atomic operations wait() and signal().A block of statements that accesses a shared variable and that only one process may execute at a time.
TypeAn OS / synchronisation tool — a variable plus two operations, usable in plain C.A programming-language facility; needs compiler support (a monitored or protected variable).
PurposeOrder concurrent access and count resource instances; expresses both mutual exclusion and condition synchronisation.Enforce mutual exclusion over one specific shared variable, and nothing else.
ControlExplicit: the programmer calls wait before and signal after; unbalanced calls cause deadlock or lost exclusion.Automatic: entering the region acquires the guard and leaving it releases it.
ValueAny non-negative integer (0 or 1 if binary); the value carries meaning — permits left, or −(blocked processes).No user-visible value at all; the “lock” is implicit in the construct.
ExtraReusable across many resources and shareable between unrelated processes.Bound to the variable it protects; cannot wait on an external event.
Exam tipThe 5.5 marks on this question split roughly as definition (1) + wait/signal with the busy-wait code (1) + types table (1) + blocking implementation with the wait queue (1) + semaphore-vs-critical-region table (1.5). The last two are printed as tables in the book — write them as tables.
Unit II · Semaphores

Show how you would implement a counting semaphores using binary semaphores.

Older PYQ — syllabus gap End Term May–June 2018 · Q.5(b) 6 Marks Medium
Show answer

Purpose

Some kernels supply only a binary semaphore (a mutex). The question asks you to build a counting semaphore of initial value K out of nothing but mutexes plus one ordinary integer — i.e. to show that the “count” is book-keeping, not hardware.

The variables

VariableKindInitialRole
mutexbinary semaphore1Guards delay, which makes the counting logic atomic.
sbinary semaphore0The “hold here” line: surplus processes block on it. Only ever touched while mutex is held.
delayshared integer0How many processes are currently standing inside the entry protocol.
Kinteger constantKHow many permits the counting semaphore is supposed to have.

Pseudocode

/* down() = wait(),  up() = signal() — both on BINARY semaphores */

semaphore mutex = 1;
semaphore s     = 0;
int     delay   = 0;
int     K;                        /* the counting semaphore's value */

void waitCounting(void) {         /* acts as P(S) on a K-permit semaphore */
      down(&mutex);
      delay = delay + 1;
      if (delay <= K)                   /* a permit is still free  */
            up(&mutex);                 /* ... so just walk through */
      else {
            up(&mutex);                 /* NEVER sleep holding mutex */
            down(&s);                   /* ... queue on s instead     */
      }
      delay = delay - 1;
}

void signalCounting(void) {       /* acts as V(S) */
      up(&s);                       /* release one waiter, or bank a permit */
}

Walkthrough. Every arriving process first locks mutex and increments delay, so delay counts how many processes are at the counter. If that number is still within the K permits, it drops the mutex and proceeds — it has “consumed a permit”. Otherwise it releases the mutex before sleeping (critical: holding it while blocked would freeze everybody else out) and waits on s. A process leaving the resource executes up(&s), which wakes exactly one queued process — or, if none is queued, simply leaves s at 1 so the next arrival passes straight through. The closing line delay = delay - 1 is executed by a process when it resumes, which is what keeps delay equal to the number of processes actually standing at the counter.

Native counting semaphoreBinary-semaphore construction
One variable S, range 0..K (and negative when processes are parked)Two binary semaphores + one integer delay
wait/signal are primitives of the systemThey become procedures composed of down/up on mutexes
Cost: one atomic operationCost: up to four atomic operations per entry
Cannot deadlock on its own guardDeadlocks if mutex is held while blocking on s

Limitation to state: the construction is correct but heavier, and it is order-sensitive — up(&mutex) must happen before down(&s); reverse them and the sleeper keeps the guard and the whole system wedges.

Unit II · Semaphores vs Monitors

What is the difference between Semaphores and Monitors? Explain with suitable examples.

Older PYQ — syllabus gap End Term May–June 2018 · Q.1(d) 2.5 Marks Medium
Asked in: End Term May–June 2018 Q.1(d) (2.5 marks), and again as Q.2 of the book’s 2016 section (Marks: not clearly visible). Related: Q.4(b) of the Jul-2016 paper, “Define a monitor?” (Marks: not clearly visible), whose printed answer supplies the definition used below. Never asked in the 2023–2025 papers.
Show answer

Monitor — as printed in the Jul-2016 answer: “In concurrent programming, a monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait (block) for a certain condition to become true. Monitors also have a mechanism for signalling other threads that their condition has been met. A monitor consists of a mutex (lock) object and condition variables …” At most one thread may be executing any method of the monitored object at a time.

monitor BankAccount {
      double balance = 0;
      condition enough;                  /* a condition variable */

      procedure deposit(double x) {
            balance += x;
            enough.signal();             /* wake a waiting withdrawer */
      }
      procedure withdraw(double x) {
            while (balance < x)         /* wait for a CONDITION, not a permit */
                  enough.wait();         /* releases the monitor lock while waiting */
            balance -= x;
      }
}

Read it. The programmer writes ordinary procedures; the compiler hides the wait(mutex)/signal(mutex) pair around every entry and exit. The only explicit synchronisation left is on a condition variable, and enough.wait() both blocks the caller and releases the lock — something a semaphore cannot do on its own.

Semaphore vs Monitor
BasisSemaphoreMonitor
LevelOS / kernel synchronisation primitiveProgramming-language construct (a module or class-like abstraction)
NatureAn integer plus two atomic operationsPrivate data + procedures, wrapped in an implicit lock
Mutual exclusionProgrammer must call wait/signal correctly by handAutomatic and invisible — enforced at each procedure entry and exit
Waiting on a conditionOnly on the semaphore’s count; needs an extra variable and a busy loop to test a conditionBuilt-in condition variables with wait()/signal()
Scope of useBetween unrelated processes, and across user and kernel modeInside one program/module, among its own threads
Failure modeEasy to get wrong: missing signal ⇒ deadlock; wrong order ⇒ deadlockHarder to break, but a condition wait without a re-testing loop can miss its wakeup
Requires compiler supportNoYes
Typical exampleProducer–Consumer with empty, full, mutexA thread-safe bank account, a bounded-buffer class, the dining-philosophers state array

The Jul-2016 answer also records the two properties worth quoting: a monitor provides mutual exclusion plus the ability to block until a condition becomes true, and at most one thread may be executing any of its methods at a time.

H. Busy Waiting vs Blocking

Synchronization
Unit II · Busy Waiting

What is the meaning of the term busy waiting? Can busy waiting be avoided altogether?

Recent PYQ — must do End Term Dec 2024 · Q.1(c) 5 Marks Very high
Asked in: End Term Dec-2024 Q.1(c) (5 marks) · Mid Term Nov-2023 Q.1(e) (2 marks — “What is busy waiting? How to overcome busy waiting using semaphore operations.”) · Older papers: End Term Jul-2023 Q.1(a) (2.5 — same wording plus “What other kinds of waiting are there in an operating system?”) and End Term May–June 2018 Q.1(e) (2.5 — “Define the term busy waiting. Can busy waiting be avoided altogether? Explain your answer.”).
Show answer

Definition. Printed wording of the Nov-2023 answer: busy waiting is a synchronisation technique in which a process waits and continuously checks the entry condition; it is also called busy looping or spinning. The process is ready as far as the scheduler is concerned — it is running, executing a loop that does nothing but re-read a shared variable hoping it has changed.

The Dec-2024 answer illustrates it with exactly this fragment — a process holding no lock, polling a flag instead of sleeping:

while (lock == 1) {
      /* do nothing – just keep checking */
}

Read it: as long as somebody else holds the lock, this loop consumes a whole time slice doing nothing useful. Where the lock holder is on another core that is tolerable; where the lock holder is ready but not running on the same single CPU, the spinner is actively preventing the holder from finishing — the worst case, called priority inversion by spinning.

What it costs

  • CPU cycles burned by every waiting process, which is cycles nobody else can use — throughput falls even though nothing is “blocked”.
  • Memory-bus traffic: each spin re-reads (and with Test-and-Set / Swap re-writes) the same lock word, slowing the very process that holds it.
  • Unfairness: spinning gives no ordering, so a process may be overtaken indefinitely — the bounded-waiting failure of Section F.

“Can busy waiting be avoided altogether?” — the honest answer is no

Busy waiting can be avoided, but only at the cost of blocking, and blocking is not free: putting a process to sleep means saving its context, moving it to a device or semaphore queue, later waking it, placing it on the ready queue and performing at least one context switch back. If the lock will be released in a few hundred nanoseconds, all that work costs far more than a short spin would have. So the question “avoided altogether?” has a two-part answer: (i) yes — replace the spin loop with a wait queue, which is exactly the blocking semaphore of Section G; (ii) no — because some form of waiting is unavoidable whenever a resource is contested, and for very short critical sections spinning is strictly cheaper. On a uniprocessor it is worse than useless for a long wait, because the spinner starves the process it is waiting for; on a multiprocessor the holder runs in parallel, so a brief spin is often the best policy.

The four methods of avoiding busy waiting (as printed in Dec-2024)

Method | Description
MethodDescriptionWhat the waiting process does
Blocking / SleepThe process surrenders the CPU using the wait() / signal() pair, and is resumed only when the resource is handed back.Leaves the CPU entirely — moved to the semaphore’s wait queue.
Mutexes with condition variablesA thread that finds its precondition false calls cond.wait(), which atomically releases the mutex and blocks; the holder signals it later.Sleeps, and crucially releases the lock while sleeping, so others can make progress.
InterruptsInstead of polling a flag, the process registers interest and is resumed by an interrupt / signal when the event occurs.Blocked; the hardware or kernel does the checking, and the wakeup is event-driven.
Semaphore (with blocking)The wait-queue implementation of wait(): decrement, and if the value goes negative add the PCB to S->list and call block().Blocked on a named queue; signal() performs wakeup(P).

“How to overcome busy waiting using semaphore operations” (Nov-2023, 2 marks)

Give the two implementations side by side and name the change. The busy-wait definition is wait(S){ while(S<=0); S--; } signal(S){ S++; } — the empty while is the spin. The blocking definition replaces that loop with a queue operation: wait(S){ S->value--; if (S->value < 0) { add this process to S->list; block(); } } and signal(S){ S->value++; if (S->value <= 0) { remove a process P from S->list; wakeup(P); } }. The spin test becomes a single arithmetic test plus a scheduler call, so the waiting process is no longer on the CPU.

The spinlock trade-off — which to pick

BasisSpinlock (busy wait)Blocking lock (sleep lock)
Waiting mechanismLoop that re-tests the lock wordWait queue + block() / wakeup()
Best when the wait isVery short — shorter than a context switchLong — I/O, resource really contended
CPU utilisation while waitingWasted (one core burned per waiter)Free for other work
Context-switch costNone until the lock is takenTwo switches per block/wake pair
Works on a uniprocessor?Only if the holder can still run; a spinner can starve itYes — that is its home ground
Works on a multiprocessor?Yes, and often preferred for tiny kernel regionsYes
Fairness / bounded waitingNot without an explicit queueYes, with a FIFO queue
Can it be used in an interrupt handler?Yes — the only option, since you cannot sleep thereNo — blocking is illegal at interrupt level
Where used in real kernelsVery short critical regions (a counter, a list insert)Semaphores, mutexes, condition variables, file locks
Exam tipFor the 5-mark Dec-2024 version: definition, the printed while (lock == 1) fragment, a two-line statement of the cost, the four-method table, and then the direct answer “no — busy waiting cannot be avoided altogether, because blocking has its own cost and short waits make spinning cheaper”. For the 2-mark Nov-2023 version, definition + the two semaphore implementations is enough.

I. The Producer–Consumer Problem

Classical Problems
Unit II · Producer–Consumer

Producer–Consumer problem — unbounded versus bounded buffer, and the solution using three semaphores.

Recent PYQ — must do End Term Jan 2024 · Q.4(a) 7.5 Marks (whole question) High
Asked in: End Term Jan-2024 Q.4(a) (7.5 marks) — producer–consumer is not its own question there; it appears inside the IPC answer, which the notes record as “Producer–Consumer example with unbounded vs bounded buffer explanation”. Older papers: End Term May–June 2018 Q.5(a) (6.5 marks) — “Describe Producer Consumer problem and Dining philosopher problem with its possible solution.” Elsewhere in the older book the words appear only inside a device-management buffering answer.
Show answer

The problem. Two processes share a fixed-size buffer and cooperate: the producer generates items and drops them into the buffer; the consumer removes items one at a time and processes them. Three constraints make it non-trivial: the consumer must not take from an empty buffer, the producer must not write into a full buffer, and the two must never update in / out / counter at the same instant — that last one is a plain race condition of Section A.

Unbounded versus bounded buffer
BasisUnbounded bufferBounded buffer
AssumptionThe producer may create an unlimited number of items; there is always room.The buffer holds at most n items; there may be no room.
ImplementationA linked list (or a practically infinite array) that grows as items arrive.A fixed circular array of n slots plus in, out and counter.
What the producer must checkNothing except mutual exclusion over the list itself.That at least one slot is free — otherwise it must wait.
What the consumer must checkThat the list is non-empty.That at least one slot is full — otherwise it must wait.
Synchronization neededWeak: one lock over the list is enough.Strong: mutual exclusion and two counting semaphores.
Failure modeRuns out of memory, not correctness.Deadlock or lost item if the protocol is wrong; blocks by design when full/empty.
Realistic?No — memory is finite, so it is a simplification.Yes — this is the version asked in exams and used in kernels.

The bounded buffer as a circular array

#define N 6                     /* buffer size */
item   buffer[N];
int    in  = 0;                 /* next free slot for the producer */
int    out = 0;                 /* next full slot for the consumer */
int    counter = 0;             /* number of items currently in the buffer */

Read it. in and out are indices, not pointers: after writing at slot N−1 the producer wraps to slot 0 with in = (in + 1) % N, and the consumer does the same with out. counter is the shared item count — and because counter++ and counter-- are exactly the read–modify–write pattern of Section A, the naive one-lock version of this code has the classic lost-update bug (counter stuck at 1 while both slots are filled, so the consumer reads garbage).

Fig I-1 · Circular buffer of N = 6 slots: producer writes at in, consumer reads at out, both wrap modulo 6
PRODUCER produce an item CONSUMER consume an item buffer [ ] — shared array of N = 6 slots buffer[0]item buffer[1]item buffer[2]free buffer[3]free buffer[4]free buffer[5]free out = 0 in = 2 read buffer[out]; out = (out + 1) % 6 write buffer[in]; in = (in + 1) % 6 full empty counter 2 slots full counter = 2 filled slots. Producer waits when counter = 6; consumer waits when counter = 0.
How to draw this in exam
  1. Draw a row of six boxes labelled buffer[0] … buffer[5] and shade only the filled ones.
  2. Put out under the oldest filled slot and in under the first free slot.
  3. Arrow the producer into in and the consumer out of out, writing the two update formulas beside them.
  4. Add the wrap-around note “(index + 1) % N” — that modulo is what makes the array circular.

Solution using three semaphores

SemaphoreInitial valueCountsType
mutex1Permission to touch the buffer structures (in, out, counter)Binary — gives mutual exclusion
emptynNumber of free slots — a permit the producer must spendCounting — synchronises on fullness
full0Number of filled slots — a permit the consumer must spendCounting — synchronises on emptiness
semaphore mutex = 1;      /* guards the buffer itself   */
semaphore empty = n;      /* n free slots at the start  */
semaphore full  = 0;      /* no items at the start      */

PRODUCER                          CONSUMER
while (true) {                    while (true) {
   item = produce();
   wait(empty);   /* a slot free? */   wait(full);    /* an item ready? */
   wait(mutex);   /* alone in buf */   wait(mutex);
   buffer[in] = item;                  item = buffer[out];
   in = (in + 1) % n;                  out = (out + 1) % n;
   signal(mutex);                      signal(mutex);
   signal(full);  /* announce item  */  signal(empty); /* a slot freed */
   consume(item);                      process(item);
}                                   }

Walkthrough. The producer first spends an empty permit; if all n slots are occupied empty is 0 and it blocks there — it never even touches the buffer. Only then does it take mutex, so no other updater is inside. It writes the item, advances in modulo n, releases mutex, and finally signals full, which is what wakes a consumer that had been parked on an empty buffer. The consumer runs the mirror image: spend a full permit, lock, take the item at out, advance, unlock, and signal empty to hand a slot back to a parked producer. The two counting semaphores therefore carry the buffer’s state between them — every item in the buffer is a spent empty permit that has not yet been spent again as full, so empty + full ≤ n, with equality whenever no process is half-way through its pair of waits.

Verification note — the order of the two waitsWrite wait(empty); wait(mutex);, never wait(mutex); wait(empty);. With the second order, a producer that finds the buffer full holds mutex while blocking on empty; the consumer needs that same mutex to remove an item, so neither can move — a deadlock created by the protocol itself, not by the data. The same rule explains why signal() comes after signal(mutex) in the code above: the permit is announced once the shared structure is already consistent.

Extending the answer: for k producers and m consumers the same three semaphores work unchanged — mutex serialises the buffer, while empty/full count slots and items for everybody. For the unbounded version the empty semaphore is unnecessary (a slot is always available) and only full plus mutex remain.

J. The Dining Philosophers Problem

Classical Problems
Unit II · Dining Philosophers

Explain the Dining Philosopher Problem. Solve the Dining Philosopher Problem using Semaphores. Provide a pseudo-code.

Recent PYQ — must do Mid Term Oct 2025 · Q.3(a) 5 Marks Very high
Asked in: Mid Term Oct-2025 Q.3(a) (5 marks — the only one of these whose full answer is actually printed in the recent book) · Mid Term Oct-2024 Q.3(b) (5 marks — printed as “What is the Dining Philosophers Problem, and outline a solution using semaphores.”, answer “Refer Q.3(a) Mid Term Exam 2025”) · Mid Term Nov-2023 Q.3(b) (Marks: not clearly visible — “What is Dining Philosopher problem? Discuss solution to Dining Philosopher’s using semaphores.”, printed as a cross-reference “Refer Q5(a) End Term Exam 2018 (Pg.no. 18-2018)”) · End Term Dec-2025 Q.5(b) (5 marks — “What is a critical section and critical section problem? Describe the dining philosopher’s problem and write the solution of the problem.”, whose printed answer sends you back to “Refer Q.3(a) from Mid Term Exam Oct. 2025”, i.e. to this card). Older papers: End Term May–June 2018 Q.5(a) (6.5 — combined with Producer–Consumer, and it is the answer that carries the pseudocode and the circular-table diagram) · End Term Jul-2016 Q.4(c) (Marks: not clearly visible — “Explain the Dining Philosophers Problem?”) · End Term Jul-2023 Q.4(b) (6 — “Explain the Dining Philosophers classical IPC problem and its solution.”).
Show answer

Problem statement — as printed

The problem is due to E. W. Dijkstra (“Co-operating Sequential Processes”). Printed wording: “There is a dining room containing a circular table with five chairs. At each chair is a plate, and between each plate is a single chopstick. In the middle of the table is a bowl of spaghetti. Near the room are five philosophers who spend most of their time thinking, but who occasionally get hungry and need to eat so they can think some more. In order to eat, a philosopher must sit at the table, pick up the two chopsticks to the left and right of a plate, then serve and eat the spaghetti on the plate.”

The rules printed alongside it: a philosopher may THINK indefinitely; every philosopher who EATs will eventually finish; chopsticks may be PICKED UP and PUT DOWN in either order and non-deterministically, but those are atomic actions, and two philosophers cannot use a single CHOPSTICK at the same time. The task is to design a protocol satisfying the liveness condition “any philosopher who tries to EAT, eventually does.”

Fig J-1 · The round table: five philosophers, five plates, one chopstick between each pair of plates, one bowl in the middle
round table bowl of spaghetti P0 P1 P2 P3 P4 C0 C1 C2 C3 C4 THINKING hungry holds one stick P i needs C i (left) and C (i+1 mod 5) (right) — both at once. The deadlock cycle P0 holds C0, waits for C1 P1 holds C1, waits for C2 P2 holds C2, waits for C3 P3 holds C3, waits for C4 P4 holds C4, waits for C0 circular wait — nobody can eat. the wait-for cycle
How to draw this in exam
  1. Draw a big circle for the table and a small shaded circle at its centre for the bowl of spaghetti.
  2. Put five seats equally spaced round it (use 72° spacing) and label them P0 … P4.
  3. Draw five short thick lines between adjacent seats — one chopstick per gap — and label them C0 … C4.
  4. Beside the figure write the pairing: Pi needs Ci and C(i+1) mod 5.
  5. For the deadlock, add five small arrows round the ring to show the circular wait.

The naive pseudocode — as printed in the papers

process P[i]
  while true do
  {  THINK;
     PICKUP(CHOPSTICK[i], CHOPSTICK[i+1 mod 5]);
     EAT;
     PUTDOWN(CHOPSTICK[i], CHOPSTICK[i+1 mod 5])
  }

Read it. Each of the five processes THINKs, then atomically picks up its left chopstick CHOPSTICK[i] and its right one CHOPSTICK[i+1 mod 5], EATs, and puts both down. The mod 5 is what wraps philosopher 4’s right-hand chopstick back to chopstick 0.

The semaphore statement of the same protocol

semaphore chopstick[5];        /* every one initialised to 1 = "on the table" */

do {
      wait(chopstick[i]);                 /* pick up the LEFT  one  */
      wait(chopstick[(i + 1) % 5]);       /* pick up the RIGHT one  */
      /* EAT */
      signal(chopstick[i]);               /* put the LEFT  back */
      signal(chopstick[(i + 1) % 5]);     /* put the RIGHT back */
      /* THINK */
} while (true);

Walkthrough. A chopstick is modelled as a binary semaphore: wait takes it (only one philosopher can succeed), signal returns it. Mutual exclusion per chopstick is therefore perfect — two philosophers can never hold the same stick, which is the rule the problem states. That is precisely the problem: the protocol guarantees the wrong thing. It guarantees exclusion but nothing about progress for the group.

Why it deadlocks

Suppose all five philosophers become hungry at roughly the same instant and all five execute their first wait. Each grabs its left chopstick — all five succeed, because each left chopstick is unique to that philosopher. Now all five execute their second wait for the right chopstick, and each of those sticks is exactly the neighbour’s left stick, which is already held. Every philosopher holds one resource and waits for one held by the next: a circular wait round the whole table, nobody can eat, nobody puts a stick down, and the system is wedged with five hungry processes and a full bowl. As the printed answer puts it, you “will quickly encounter the same deadlock and livelock scenarios we saw in the mutual exclusion problem, but you will quickly see in this case that mutual exclusion is too primitive a synchronization mechanism for solving this problem.”

Livelock is the mirror-image failure: suppose instead every philosopher puts down its left stick when the right one is unavailable, waits a moment, and tries again in the same synchronised rhythm. They then repeat the pickup simultaneously forever — all are active, all are making “effort”, and none ever eats. So the fix must break one of the four deadlock conditions, usually circular wait.

Three standard fixes

Fix 1 — asymmetric pickup (odd–even). Make odd-numbered philosophers take the right chopstick first. Two neighbours can then never both hold the stick between them, so a cycle around the table cannot form; the circular-wait condition is destroyed.

do {
      if (i % 2 == 0) {                        /* even: left then right  */
            wait(chopstick[i]);
            wait(chopstick[(i + 1) % 5]);
      } else {                                 /* odd:  right then left  */
            wait(chopstick[(i + 1) % 5]);
            wait(chopstick[i]);
      }
      /* EAT */
      signal(chopstick[i]);
      signal(chopstick[(i + 1) % 5]);
      /* THINK */
} while (true);

Fix 2 — allow at most four philosophers at the table. With n seats but only n − 1 diners, at least one stick always stays free, so at least one philosopher can complete a pair and eventually eat; mutual exclusion over the room breaks the cycle by limiting concurrency. This is the answer the notes record as the “K philosophers” variant.

semaphore roomService = 4;        /* at most FOUR may sit down  */

do {
      wait(roomService);          /* 1. claim a seat             */
      wait(chopstick[i]);         /* 2. then take both sticks    */
      wait(chopstick[(i + 1) % 5]);
      /* EAT */
      signal(chopstick[i]);
      signal(chopstick[(i + 1) % 5]);
      signal(roomService);        /* 3. leave the table          */
      /* THINK */
} while (true);

Fix 3 — an atomic pickup wrapper. The problem says PICKUP is a single atomic action, so implement it as one: wrap the two waits inside a further mutex so that no other philosopher can be partway through a pickup while you are doing yours. Then a partial pair can never be observed, and the all-five-holding-their-left-stick state is unreachable.

semaphore chopstick[5];           /* all 1 */
semaphore pickup    = 1;          /* makes the pair acquisition atomic */

do {
      wait(pickup);               /* only one philosopher picks up at a time */
      wait(chopstick[i]);
      wait(chopstick[(i + 1) % 5]);
      signal(pickup);             /* release BEFORE eating, not after */
      /* EAT */
      signal(chopstick[i]);
      signal(chopstick[(i + 1) % 5]);
      /* THINK */
} while (true);

Note the one price of fix 3: only one philosopher may collect sticks at a time, so pickup becomes a bottleneck — the cleanest statement of the trade-off is that we have bought liveness by giving up some concurrency. The equivalent high-level formulation is the monitor / state-array solution (int state[5] with test(i) checking that both neighbours are not eating, and per-philosopher condition variables), which is the “answer code” the older book prints for the 2017 dining-philosophers program.

The three fixes compared
FixDeadlock condition brokenConcurrent eaters possibleBounded waitingCost
Odd–even asymmetric pickupCircular waitAt most 2 can eat at once, and they are never neighboursYesAsymmetric code is easy to mis-write; must be applied to every diner.
At most four at the tableHold-and-wait is bounded; the cycle cannot closeUp to 2 as well, but with a global counterYesNeeds an extra counting semaphore; generalises to “n − 1”.
Atomic pickup wrapperNo process can hold one stick while waiting for the next1 pickup at a time (eating still overlaps)YesPickup serialised — the least concurrent of the three.
Naive two waits (given code)None—DeadlocksCorrect mutual exclusion, wrong liveness.
Exam tipThe Oct-2025 answer that Nov-2023 and Oct-2024 both point to is structured in exactly five beats: the printed problem statement, the round-table diagram, the PICKUP(CHOPSTICK[i], CHOPSTICK[i+1 mod 5]) pseudocode, the liveness condition quoted verbatim, and semaphore definitions with DOWN(S) = “wait until S > 0, then decrement S” / UP(S) = “increment S”, closing with the note that in time-sharing “waiting” is implemented by the OS putting processes on a wait-list. Add one of the three fixes — that is the part that separates a 5/5 from a 3/5.

K. The Sleeping Barber (Barber Shop Problem)

Classical Problems
Unit II · Sleeping Barber

The Sleeping Barber (barber shop) problem — statement, shared data, semaphore solution and explanation.

Syllabus only — no direct PYQ Not asked in either book Safety
Show answer
Why this section is hereThe Sleeping Barber is named in your syllabus — the UNIT-II line of the syllabus page reads “Process Synchronization: Mutual exclusion, software solution to Mutual exclusion Problem, hardware solution to Mutual exclusion problem, semaphores, Critical section problems. Case study on Dining philosopher problem, Barber shop problem etc.” — but it is asked in neither book. All four extraction passes record the same result: “Sleeping Barber — NOT FOUND (the phrase appears only on the syllabus page)”, and the coverage matrix row 33 marks it “none / none / COVER FOR SAFETY”. One clean problem statement plus one semaphore solution is cheap insurance.

Problem statement

A barbershop has one barber, one barber chair, and a waiting room with a fixed number N of chairs. The rules are:

  1. If there are no customers, the barber goes to sleep in the barber chair and must not be woken except by the arrival of a customer.
  2. A customer who arrives wakes the barber if he is asleep and sits in the barber chair; the barber cuts that customer’s hair and then repeats the cycle.
  3. A customer who arrives while the barber is busy sits in the waiting room if a chair is free.
  4. A customer who arrives and finds all N waiting chairs occupied leaves the shop and does not come back.
  5. Only one haircut may be in progress at a time — the barber is a single, non-shareable resource.

The technical difficulty is the handshake: the barber must sleep only while there really is nobody to serve, must be woken by exactly one arriving customer, and must not race with a customer who arrives at the same instant the previous haircut ends. A naive pair of tests such as “barber: if (waiting > 0) …” and “customer: if (barber asleep) …” produces both lost customers (asleep barber, waiting count already read as 0) and a barber waiting for a customer who is waiting for the barber.

Fig K-1 · Layout of the shop: entrance, waiting room with N chairs, one barber chair, and the two semaphores that join them
THE SHOP door customer arrives WAITING ROOM — N chairs 1 2 3 free free waiting = 3 of 5 — a 6th arrival leaves next BARBER CHAIR barber asleep until a customer arrives one haircut at a time customers = 0 barber sleeps on this semaphore barbers = 0 customer waits on this semaphore N = 5 signal(customers) signal(barbers) mutex guards waiting
How to draw this in exam
  1. Draw the shop as one big rectangle with a door on the left wall.
  2. Inside, one box “waiting room — N chairs” with a few chairs, some shaded to show waiting.
  3. Right of it, a smaller box “barber chair”, annotated “barber asleep”.
  4. Underneath, draw the two semaphores customers and barbers and arrow the handshake between them.
  5. Write the one-line rule next to the door: “waiting room full ⇒ customer leaves”.

The shared data

ObjectKindInitialMeaning
CHAIRSconstant = Ne.g. 5Number of seats in the waiting room.
customerscounting semaphore0“There is a customer waiting to be served.” The barber blocks here — this is his sleep.
barberscounting semaphore0“The barber is ready to cut my hair.” The customer blocks here.
mutexbinary semaphore1Guards waiting, because several customers may arrive concurrently.
waitingshared integer0How many customers are seated in the waiting room.

Pseudocode

#define CHAIRS 5                     /* seats in the waiting room    */
semaphore customers = 0;             /* how many clients to serve    */
semaphore barbers   = 0;             /* how many haircuts ready      */
semaphore mutex     = 1;             /* protects "waiting"           */
int       waiting   = 0;

/* ---------------- BARBER ---------------- */
while (true) {
      wait(customers);                /* sleep until a customer arrives   */
      wait(mutex);
      waiting = waiting - 1;          /* one fewer in the waiting room    */
      signal(barbers);                /* "come and sit down"              */
      signal(mutex);
      cut_hair();                     /* OUTSIDE the mutex — it is slow   */
}

/* ---------------- CUSTOMER -------------- */
wait(mutex);
if (waiting < CHAIRS) {
      waiting = waiting + 1;          /* take a seat                     */
      signal(customers);              /* wake the barber if asleep       */
      signal(mutex);
      wait(barbers);                  /* wait for the chair to be free   */
      get_haircut();
} else {
      signal(mutex);                  /* no seat: release lock and leave  */
}

Explanation of the handshake. The barber’s very first line, wait(customers), is the sleep: with no customer, the counting semaphore is 0, so the barber is put on the semaphore’s wait queue by the blocking implementation of Section G and consumes no CPU at all — no busy waiting. A customer who arrives takes mutex, finds a free chair, increments waiting, and executes signal(customers); that increments the count and wakes exactly one sleeping barber. The barber then decrements waiting, signals barbers to say “the chair is ready”, drops mutex, and only then starts cutting — deliberately outside the mutex, because a haircut is long and holding mutex through it would block arriving customers from even checking for a seat. Symmetrically the customer parks on wait(barbers) and is released into the chair. If the waiting room is already full the customer releases mutex and walks out, never touching either synchronisation semaphore.

The four states, and who is blocked where
SituationBarberCustomerSemaphore state
Shop emptyAsleep—customers = 0, barbers = 0, waiting = 0
First arrivalWoken, cuttingIn the chaircustomers = 0 (spent), barbers = 0 (spent)
Second arrival during a cutCuttingSeated, waitingcustomers = 1, waiting = 1
Arrival when all N chairs usedCuttingLeaves the shopwaiting = N; the if fails, no signal sent

Why it is a good exam topic to have read once: it combines everything on this page — a shared variable protected by a binary semaphore, two counting semaphores used for direction of synchronisation rather than counting resources, blocking instead of busy waiting, and a producer–consumer-shaped buffer (the waiting room) between an unbounded producer (arrivals) and a single slow consumer (the barber). If it is ever asked, the marks are in the handshake; write the two wait/signal pairs and explain why cut_hair() sits outside the mutex.

Unit II Synchronization Checklist

Track

Ticks are saved in this browser and survive a refresh. Progress also feeds the dashboard.