Operating Systems

Unit 4: Process Synchronization & Threads

From race conditions to thread-safe code โ€” master critical sections, semaphores, classical synchronization problems, and multithreading to build reliable concurrent systems.

โฑ๏ธ Time to Complete: 7 hrs theory + 5 hrs lab  |  ๐Ÿ’ฐ Earning Potential: โ‚น8Kโ€“โ‚น25K/month  |  ๐Ÿ“ 30 MCQs (Bloom's Mapped)

๐Ÿ’ผ Jobs this unlocks: Backend Engineer (โ‚น6โ€“12 LPA)  |  Systems Programmer (โ‚น8โ€“15 LPA)

Section A

Opening Hook โ€” Race Condition in Action!

๐ŸŽฌ The BookMyShow Bug That Sold One Ticket Twice

Picture this: Friday evening, 7 PM. Rahul in Mumbai and Sneha in Pune are both staring at the last available ticket for Jawan at PVR Phoenix. Both see "1 seat available." Both click "Pay Now" through Paytm at the exact same millisecond.

The payment gateway processes both requests. Both receive confirmation SMS: "๐ŸŽซ Booking confirmed!" But there's only ONE seat. When Rahul arrives at the theatre, his ticket is invalid. Chaos. Refunds. 1-star reviews.

What went wrong? The code that checks seat availability and deducts the seat count was NOT protected. Two threads read the same value (seats = 1), both passed the check (1 > 0), both decremented (seats = 0), but the seat was sold twice. This is a race condition โ€” and it's exactly what process synchronization prevents.

Every payment gateway (Razorpay, Paytm, PhonePe), every booking system (IRCTC, BookMyShow, MakeMyTrip), every banking app runs concurrent code. Without synchronization, your money isn't safe. This chapter teaches you how to make it safe.

๐Ÿ‡ฎ๐Ÿ‡ณ BookMyShow๐Ÿ‡ฎ๐Ÿ‡ณ Paytm๐Ÿ‡ฎ๐Ÿ‡ณ IRCTC๐Ÿ‡ฎ๐Ÿ‡ณ Razorpay๐Ÿ‡ฎ๐Ÿ‡ณ PhonePe๐Ÿ‡ฎ๐Ÿ‡ณ Zerodha
In 2015, IRCTC's Tatkal booking system crashed because 15 million users hit "Book" simultaneously. The root cause? Inadequate synchronization in the ticket allocation module. IRCTC later implemented mutex-based locking and queue-based token systems โ€” concepts you'll learn in this very chapter. Today, IRCTC handles 25+ million bookings daily using these techniques.
Section B

Learning Outcomes โ€” Bloom's Taxonomy Mapped

Bloom's LevelLearning Outcome
๐Ÿ”ต RememberList the 3 requirements of the critical section problem; define semaphore, mutex, monitor, and thread
๐Ÿ”ต UnderstandExplain how Peterson's Algorithm ensures mutual exclusion; differentiate binary vs counting semaphores with Indian analogies
๐ŸŸข ApplyImplement Producer-Consumer using POSIX semaphores in C; write Pthreads programs with mutex locks
๐ŸŸข AnalyzeTrace race conditions in concurrent code; compare user-level vs kernel-level threads; analyze deadlock scenarios in Dining Philosophers
๐ŸŸ  EvaluateEvaluate which synchronization primitive is best for a given real-world system (banking, booking, etc.)
๐ŸŸ  CreateDesign a thread-safe booking system; implement Dining Philosophers with deadlock detection
Section C

Concept Explanation โ€” Process Synchronization & Threads from Scratch

1. Concurrent & Cooperating Processes

Concurrent processes are processes that execute during overlapping time periods. They may run truly in parallel on multiple CPU cores, or interleave on a single core via context switching.

Processes are of two types based on interaction:

TypeDefinitionExample
IndependentCannot affect or be affected by other processes. No shared data.Two students writing separate exams in different rooms
CooperatingCan affect or be affected by other processes. Share data/resources.Two chefs sharing one stove in a kitchen โ€” they must coordinate

Why cooperating processes?

  • Information sharing: Multiple users accessing a shared database (e.g., multiple IRCTC agents selling same train seats)
  • Computation speedup: Breaking a task into sub-tasks that run in parallel
  • Modularity: Dividing system functions into separate processes
  • Convenience: A user may run editor, compiler, and browser simultaneously
UPI payments are cooperating processes. When you send โ‚น500 via PhonePe, at least 3 processes cooperate: (1) your bank's debit process, (2) NPCI's clearing process, and (3) the receiver's bank's credit process. All three must synchronize โ€” otherwise money could be debited but never credited!

2. The Critical Section Problem

A critical section is a segment of code where a process accesses shared resources (variables, files, databases). If two processes enter their critical sections simultaneously, data corruption can occur.

๐Ÿ”’ Structure of a Process with Critical Section

Pseudocode
do {
    /* ENTRY SECTION โ€” request permission to enter */

    CRITICAL SECTION
    /* Access shared resource here */

    /* EXIT SECTION โ€” release the lock */

    REMAINDER SECTION
    /* Non-critical code */

} while (TRUE);

Three Requirements for a Valid Solution

#RequirementMeaningIndian Analogy
1Mutual ExclusionIf process Pi is in its critical section, no other process can be in its critical sectionOnly ONE person in the ATM booth at a time โ€” door is locked from inside
2ProgressIf no process is in its critical section and some processes wish to enter, the selection cannot be postponed indefinitelyIf the ATM booth is empty and someone is waiting, they must be allowed in โ€” you can't say "come back tomorrow"
3Bounded WaitingThere must be a limit on the number of times other processes can enter before a waiting process gets its turnIn a bank queue, you get a token number โ€” you WILL be served; nobody can keep cutting in front of you forever
Single-lane bridge on NH โ€” only one direction at a time! Think of a narrow bridge on a national highway between two villages. Cars can only go in ONE direction at a time (mutual exclusion). If the bridge is free and cars are waiting, one side must be allowed to go (progress). And the other side can't wait forever โ€” after a fixed number of cars pass, the direction switches (bounded waiting).
Students often confuse "mutual exclusion" with "deadlock freedom." Mutual exclusion means only one process in CS at a time. Deadlock freedom (which the Progress requirement ensures) means the system doesn't get stuck with nobody in the CS when processes want to enter. Both are needed!

3. Software Solutions: Peterson's Algorithm & Bakery Algorithm

Peterson's Algorithm (2 Processes)

Peterson's Algorithm is an elegant software-only solution for 2 processes. It uses two shared variables: a flag[] array (indicating intent) and a turn variable (tie-breaker).

๐Ÿงฎ Peterson's Algorithm โ€” Pseudocode

Pseudocode
// Shared variables
bool flag[2] = {false, false};  // flag[i] = true means process i wants to enter
int turn;                        // whose turn it is (tie-breaker)

// Process P_i (i = 0 or 1, j = 1 - i)
do {
    flag[i] = true;             // I want to enter
    turn = j;                    // But I give you the chance first
    while (flag[j] && turn == j)
        ;                        // Busy wait โ€” spin until it's my turn

    /* CRITICAL SECTION */

    flag[i] = false;            // I'm done, leaving CS

    /* REMAINDER SECTION */
} while (TRUE);
Trace: How Peterson's Algorithm Works

Let's trace with P0 and P1 both wanting to enter:

  1. P0 sets flag[0] = true, turn = 1 (gives P1 priority)
  2. P1 sets flag[1] = true, turn = 0 (gives P0 priority)
  3. P0 checks: flag[1] == true AND turn == 1? turn is 0, NOT 1 โ†’ P0 enters CS โœ…
  4. P1 checks: flag[0] == true AND turn == 0? YES โ†’ P1 waits โณ
  5. When P0 finishes, sets flag[0] = false โ†’ P1's while loop exits โ†’ P1 enters CS โœ…

Correctness: โœ… Mutual Exclusion (only one enters) โœ… Progress (if one doesn't want, other enters immediately) โœ… Bounded Waiting (at most 1 wait)

Peterson's Algorithm has a limitation: It only works for 2 processes and assumes that memory reads/writes are atomic. On modern multi-core CPUs with out-of-order execution and CPU caches, it may not work correctly without memory barriers. That's why we need hardware primitives!

Bakery Algorithm (N Processes)

Lamport's Bakery Algorithm generalises the idea to N processes. It's named after the take-a-number system used in bakeries (or SBI banks!).

๐Ÿž Bakery Algorithm โ€” Pseudocode (N processes)

Pseudocode
// Shared variables
bool choosing[n];     // choosing[i] = true means process i is picking a number
int number[n];        // number[i] = ticket number of process i (0 = not interested)

// Process P_i
do {
    choosing[i] = true;
    number[i] = 1 + max(number[0], ..., number[n-1]);  // Take next ticket
    choosing[i] = false;

    for (j = 0; j < n; j++) {
        while (choosing[j])
            ;   // Wait if process j is choosing
        while (number[j] != 0 &&
               (number[j], j) < (number[i], i))
            ;   // Wait if j has a smaller ticket (or same ticket but smaller ID)
    }

    /* CRITICAL SECTION */

    number[i] = 0;    // Done โ€” release ticket

    /* REMAINDER SECTION */
} while (TRUE);
SBI Bank Token System: Walk into any SBI branch. You take a token number from the machine. Even if 50 people are waiting, the person with the lowest number gets served next. If two people somehow get the same number (rare!), the one who arrived first (lower process ID) goes first. That's the Bakery Algorithm!

4. Hardware Synchronization Primitives: TAS & CAS

Software solutions have limitations on modern hardware. CPU manufacturers provide atomic instructions โ€” instructions that execute as a single, uninterruptible unit.

Test-and-Set (TAS)

โš™๏ธ Test-and-Set โ€” Hardware Instruction

C-style Pseudocode
// This ENTIRE function executes atomically (hardware guarantee)
bool test_and_set(bool *target) {
    bool rv = *target;   // Save old value
    *target = true;      // Set to true
    return rv;            // Return old value
}

// Usage โ€” mutual exclusion for any number of processes
bool lock = false;    // Shared lock variable

do {
    while (test_and_set(&lock))
        ;                // Spin until lock is acquired

    /* CRITICAL SECTION */

    lock = false;        // Release lock

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

How it works: If lock is false (free), TAS returns false (loop exits) and sets lock = true atomically. If lock is true (held), TAS returns true (keeps spinning).

Compare-and-Swap (CAS)

โš™๏ธ Compare-and-Swap โ€” Hardware Instruction

C-style Pseudocode
// Executes atomically
int compare_and_swap(int *value, int expected, int new_value) {
    int temp = *value;
    if (*value == expected)
        *value = new_value;
    return temp;
}

// Usage
int lock = 0;

do {
    while (compare_and_swap(&lock, 0, 1) != 0)
        ;                // Spin until lock acquired

    /* CRITICAL SECTION */

    lock = 0;

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

Advantage of CAS over TAS: CAS only modifies the value if it matches the expected value, making it more flexible. It's the foundation of lock-free data structures used in high-performance systems like Zerodha's trading engine.

Both TAS and CAS use busy waiting (spinning). This wastes CPU cycles. A process waiting to enter keeps checking in a loop, consuming CPU time. That's why we need semaphores โ€” they put waiting processes to sleep instead of spinning.

5. Semaphores

A semaphore is a synchronization tool that does not require busy waiting (in its proper implementation). It's an integer variable accessed only through two atomic operations: P() (wait/down) and V() (signal/up).

๐Ÿšฆ Semaphore Operations โ€” P() and V()

Pseudocode
// P() operation โ€” also called wait() or down()
P(S) {
    while (S <= 0)
        ;           // Wait (in practice: block/sleep)
    S--;
}

// V() operation โ€” also called signal() or up()
V(S) {
    S++;
}

Key rule: Both P() and V() are atomic โ€” they execute without interruption.

Binary Semaphore (Mutex) vs Counting Semaphore

FeatureBinary Semaphore (Mutex)Counting Semaphore
Value range0 or 10 to N (any non-negative integer)
PurposeMutual exclusion โ€” only ONE process in CSControl access to a resource with N instances
AnalogySingle bathroom key โ€” only 1 person at a timeParking lot with N spaces โ€” N cars can enter
Init value1 (unlocked)N (number of available resources)
Token system at SBI bank โ€” only N customers inside! A counting semaphore is like SBI's token machine. If the bank can handle 5 customers simultaneously (N=5), the first 5 get tokens and enter. Customer 6 waits outside. When any customer finishes (signals), the next waiting customer's token activates. P() = take a token and enter (or wait). V() = return token and leave.
Pseudocode
// Mutual Exclusion using Binary Semaphore
semaphore mutex = 1;

// Process P_i
do {
    P(mutex);           // Entry section โ€” acquire lock

    /* CRITICAL SECTION */

    V(mutex);           // Exit section โ€” release lock

    /* REMAINDER SECTION */
} while (TRUE);
P comes from Dutch "proberen" (to test), V from "verhogen" (to increment). Dijkstra was Dutch! In most textbooks and code, you'll see wait() and signal() instead. In POSIX C, they're sem_wait() and sem_post().

6. Monitors

A monitor is a high-level synchronization construct โ€” essentially a class/module where only ONE process can be active inside at any time. Think of it as automatic mutual exclusion built into the data structure.

๐Ÿ›๏ธ Monitor Structure

Pseudocode
monitor MonitorName {
    // Shared data โ€” private to monitor
    int shared_var;

    // Condition variables
    condition x, y;

    // Procedures โ€” only one can execute at a time
    procedure P1() {
        // ... access shared_var safely ...
        if (need_to_wait)
            x.wait();     // Suspend and release monitor
    }

    procedure P2() {
        // ... modify shared_var ...
        x.signal();          // Wake up one waiting process
    }

    // Initialization code
    init() { shared_var = 0; }
}

Condition Variables

  • x.wait() โ€” The calling process is suspended and placed in a queue for condition x. The monitor lock is released so other processes can enter.
  • x.signal() โ€” Resumes exactly ONE process waiting on condition x. If no process is waiting, signal has no effect (unlike semaphore V which always increments).

Key difference from semaphores: A semaphore's V() always increments the count even if nobody is waiting. A monitor's signal() is lost if nobody is waiting โ€” it does NOT accumulate.

Java's synchronized keyword implements monitors. When you write synchronized(obj) { ... } in Java, you're creating a monitor. The JVM ensures only one thread executes inside that block at a time. Android apps, which run on JVM, use this extensively.

7. Classical Synchronization Problems

7.1 Producer-Consumer Problem (Bounded Buffer)

Scenario: A producer process generates data and puts it into a buffer. A consumer process takes data from the buffer. The buffer has a fixed size N.

Constraints:

  • Producer must wait if buffer is FULL
  • Consumer must wait if buffer is EMPTY
  • Only one process can access the buffer at a time (mutual exclusion)

๐Ÿ“ฆ Producer-Consumer โ€” Semaphore Solution

Pseudocode
// Shared
semaphore mutex = 1;       // Mutual exclusion for buffer access
semaphore empty = N;       // Count of empty slots (initially N)
semaphore full  = 0;       // Count of filled slots (initially 0)

// PRODUCER
do {
    // ... produce an item ...

    P(empty);      // Wait for an empty slot
    P(mutex);      // Lock the buffer

    // ... add item to buffer ...

    V(mutex);      // Unlock the buffer
    V(full);       // Signal that a filled slot is available
} while (TRUE);

// CONSUMER
do {
    P(full);       // Wait for a filled slot
    P(mutex);      // Lock the buffer

    // ... remove item from buffer ...

    V(mutex);      // Unlock the buffer
    V(empty);      // Signal that an empty slot is available

    // ... consume the item ...
} while (TRUE);
NEVER swap P(empty)/P(full) with P(mutex)! If the producer does P(mutex) before P(empty), and the buffer is full, the producer holds mutex and waits on empty โ€” but the consumer needs mutex to remove an item, causing deadlock! Always acquire counting semaphore BEFORE mutex.

7.2 Reader-Writer Problem

Scenario: A database is shared among processes. Readers only read data; writers modify data. Multiple readers can read simultaneously, but a writer needs exclusive access.

Reader Preference Solution (First Readers-Writers)

๐Ÿ“– Reader-Writer โ€” Reader Preference

Pseudocode
// Shared
semaphore rw_mutex = 1;   // Exclusive access for writers
semaphore mutex = 1;      // Protect read_count
int read_count = 0;       // Number of active readers

// WRITER
do {
    P(rw_mutex);       // Get exclusive access

    // ... write data ...

    V(rw_mutex);       // Release
} while (TRUE);

// READER
do {
    P(mutex);
    read_count++;
    if (read_count == 1)
        P(rw_mutex);   // First reader locks out writers
    V(mutex);

    // ... read data ...

    P(mutex);
    read_count--;
    if (read_count == 0)
        V(rw_mutex);   // Last reader unlocks for writers
    V(mutex);
} while (TRUE);

Problem: If readers keep arriving, writers may starve (never get access). This is called reader preference.

Writer Preference: Add another semaphore so that once a writer is waiting, no new readers are admitted. This prevents writer starvation but may starve readers.

7.3 Dining Philosophers Problem

Scenario: 5 philosophers sit around a circular table. Between each pair of philosophers is ONE chopstick (5 total). A philosopher alternates between thinking and eating. To eat, a philosopher needs BOTH chopsticks (left and right).

5 IAS officers sharing 5 single pens in a meeting! Imagine 5 IAS officers sitting in a circle at a government meeting. Between every two officers, there's one pen. Each officer needs TWO pens to sign a file. If all 5 pick up their left pen simultaneously, nobody has a right pen โ€” everyone waits forever. That's deadlock!

๐Ÿฝ๏ธ Dining Philosophers โ€” Naรฏve Solution (CAUSES DEADLOCK)

Pseudocode
semaphore chopstick[5];  // All initialized to 1

// Philosopher i (0 to 4)
do {
    // THINKING ...

    P(chopstick[i]);          // Pick up LEFT chopstick
    P(chopstick[(i+1)%5]);    // Pick up RIGHT chopstick

    // EATING ...

    V(chopstick[i]);          // Put down LEFT
    V(chopstick[(i+1)%5]);    // Put down RIGHT
} while (TRUE);

โš ๏ธ DEADLOCK: If all 5 philosophers pick up their left chopstick simultaneously, all are holding one and waiting for the other โ€” circular wait!

Solutions to Avoid Deadlock:
SolutionHow It Works
1. Allow at most 4Use a counting semaphore initialized to 4 โ€” at most 4 philosophers can attempt to eat
2. AsymmetricOdd philosophers pick left first, even pick right first โ€” breaks circular wait
3. All-or-nothingA philosopher picks up both chopsticks only if BOTH are available (inside a critical section)

๐Ÿฝ๏ธ Dining Philosophers โ€” Deadlock-Free (Asymmetric Solution)

Pseudocode
semaphore chopstick[5];  // All initialized to 1

// Philosopher i
do {
    // THINKING ...

    if (i % 2 == 0) {       // Even: pick left first
        P(chopstick[i]);
        P(chopstick[(i+1)%5]);
    } else {                // Odd: pick right first
        P(chopstick[(i+1)%5]);
        P(chopstick[i]);
    }

    // EATING ...

    V(chopstick[i]);
    V(chopstick[(i+1)%5]);
} while (TRUE);

Why it works: By making at least one philosopher pick up chopsticks in opposite order, we break the circular wait condition.

8. Precedence Graph

A precedence graph is a directed acyclic graph (DAG) where each node represents a statement/task and edges represent execution order constraints. If there's an edge from S1 to S2, then S1 must finish before S2 starts.

๐Ÿ“Š Precedence Graph โ€” Example

Given statements: S1, S2, S3, S4, S5, S6

Constraints: S1 โ†’ S2, S1 โ†’ S3, S2 โ†’ S4, S3 โ†’ S4, S4 โ†’ S5, S4 โ†’ S6

Diagram
        S1
       /  \
      S2    S3
       \  /
        S4
       /  \
      S5    S6

Meaning: S2 and S3 can run in parallel (both only depend on S1). S4 must wait for BOTH S2 and S3. S5 and S6 can run in parallel after S4.

Implementation with semaphores:

Pseudocode
semaphore a=0, b=0, c=0, d=0, e=0, f=0;

P1: S1; V(a); V(b);
P2: P(a); S2; V(c);
P3: P(b); S3; V(d);
P4: P(c); P(d); S4; V(e); V(f);
P5: P(e); S5;
P6: P(f); S6;

9. Threads โ€” Lightweight Processes

A thread is the smallest unit of CPU execution. A process can have multiple threads sharing the same code, data, and files, but each thread has its own program counter, register set, and stack.

Analogy: A process is like a restaurant. Threads are the waiters. All waiters share the same kitchen (memory), menu (code), and tables (files), but each waiter takes independent orders (has own PC and stack).

User-Level Threads vs Kernel-Level Threads

FeatureUser-Level Threads (ULT)Kernel-Level Threads (KLT)
Managed byThread library in user spaceOperating System kernel
Kernel awarenessKernel doesn't know threads existKernel manages each thread
Context switchFast (no kernel intervention)Slower (kernel mode switch)
BlockingIf one thread blocks, ALL threads in process blockOnly the blocking thread blocks; others continue
MultiprocessorโŒ Cannot use multiple CPUs (kernel sees one process)โœ… Can run on multiple CPUs in parallel
Creation costVery cheap (no system call)More expensive (system call needed)
ExampleGreen threads (Java pre-1.2), GNU PthLinux pthreads, Windows threads

Multithreading Models

Many-to-One Model
Diagram
  User Threads:   T1  T2  T3  T4
                    \  |  |  /
                     \ | | /
  Kernel Thread:      [K1]

Many user threads map to ONE kernel thread. Thread management in user space (fast), but one blocking call blocks ALL. Cannot exploit multiprocessors. Example: Solaris Green Threads.

One-to-One Model
Diagram
  User Threads:   T1   T2   T3   T4
                   |    |    |    |
  Kernel Threads: K1   K2   K3   K4

Each user thread maps to ONE kernel thread. More concurrency โ€” one thread blocking doesn't block others. Can use multiple CPUs. Overhead: Creating a user thread requires creating a kernel thread. Example: Linux pthreads, Windows threads.

Many-to-Many Model
Diagram
  User Threads:   T1  T2  T3  T4  T5
                    \  |  X  |  /
                     \ |/ \| /
  Kernel Threads:    K1   K2   K3

Many user threads map to many (fewer or equal) kernel threads. Best of both worlds โ€” no limit on user threads, can exploit multiple CPUs. Example: Solaris (LWP), HP-UX.

Scheduler Activations

Scheduler activations are a communication mechanism between the kernel and the user-level thread library. The kernel provides an upcall to the thread library whenever a significant event occurs (thread blocks, thread unblocks, new processor allocated).

The user-level thread library uses a data structure called a lightweight process (LWP) as an intermediary. Each LWP is attached to a kernel thread. The thread library schedules user threads onto available LWPs.

Benefit: Combines the speed of user-level thread management with the kernel's ability to handle blocking I/O and exploit multiprocessors.

10. Pthreads โ€” POSIX Thread API

Pthreads (POSIX Threads) is the standard C API for creating and managing threads on UNIX/Linux systems. It follows the One-to-One model on Linux.

Key Pthreads Functions

FunctionPurpose
pthread_create()Create a new thread
pthread_join()Wait for a thread to finish (like wait() for processes)
pthread_exit()Terminate calling thread
pthread_mutex_init()Initialize a mutex lock
pthread_mutex_lock()Acquire the mutex (blocks if already locked)
pthread_mutex_unlock()Release the mutex

๐Ÿงต Pthreads โ€” Basic Thread Creation Example

C
#include <stdio.h>
#include <pthread.h>

void* print_hello(void* arg) {
    int id = *((int*)arg);
    printf("Hello from thread %d\n", id);
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[3];
    int ids[3] = {1, 2, 3};

    for (int i = 0; i < 3; i++)
        pthread_create(&threads[i], NULL, print_hello, &ids[i]);

    for (int i = 0; i < 3; i++)
        pthread_join(threads[i], NULL);

    printf("All threads completed.\n");
    return 0;
}

Compile: gcc -o threads threads.c -lpthread

๐Ÿ” Pthreads โ€” Mutex Example (Thread-Safe Counter)

C
#include <stdio.h>
#include <pthread.h>

int counter = 0;
pthread_mutex_t lock;

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&lock);
        counter++;              // Critical section
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&lock, NULL);

    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Counter = %d\n", counter); // Always 200000 with mutex!
    pthread_mutex_destroy(&lock);
    return 0;
}

Without mutex: Counter would be less than 200000 due to race condition!

Section D

Learn by Doing โ€” 3-Tier Lab Structure

๐ŸŸข Tier 1 โ€” GUIDED TASK: POSIX Semaphore Producer-Consumer in C

โฑ๏ธ 60โ€“90 minutesBeginnerComplete code + compilation + output

Step 1: Create the Source File

Create a file named producer_consumer.c and enter the following code:

C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define BUFFER_SIZE 5
#define NUM_ITEMS   10

int buffer[BUFFER_SIZE];
int in = 0, out = 0;

sem_t empty_slots;   // Counts empty slots
sem_t full_slots;    // Counts filled slots
pthread_mutex_t mutex;

void* producer(void* arg) {
    for (int i = 1; i <= NUM_ITEMS; i++) {
        sem_wait(&empty_slots);        // P(empty) โ€” wait for empty slot
        pthread_mutex_lock(&mutex);    // P(mutex) โ€” lock buffer

        buffer[in] = i;
        printf("Producer: produced item %d at index %d\n", i, in);
        in = (in + 1) % BUFFER_SIZE;

        pthread_mutex_unlock(&mutex);  // V(mutex)
        sem_post(&full_slots);          // V(full) โ€” signal item available

        usleep(100000);  // 100ms delay to visualise
    }
    return NULL;
}

void* consumer(void* arg) {
    for (int i = 1; i <= NUM_ITEMS; i++) {
        sem_wait(&full_slots);          // P(full) โ€” wait for item
        pthread_mutex_lock(&mutex);    // P(mutex)

        int item = buffer[out];
        printf("Consumer: consumed item %d from index %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;

        pthread_mutex_unlock(&mutex);  // V(mutex)
        sem_post(&empty_slots);        // V(empty) โ€” signal slot freed

        usleep(150000);  // 150ms delay
    }
    return NULL;
}

int main() {
    sem_init(&empty_slots, 0, BUFFER_SIZE);
    sem_init(&full_slots, 0, 0);
    pthread_mutex_init(&mutex, NULL);

    pthread_t prod_thread, cons_thread;
    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&cons_thread, NULL, consumer, NULL);

    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    sem_destroy(&empty_slots);
    sem_destroy(&full_slots);
    pthread_mutex_destroy(&mutex);

    printf("\nAll items produced and consumed successfully!\n");
    return 0;
}

Step 2: Compile

Terminal
gcc -o producer_consumer producer_consumer.c -lpthread

Step 3: Run

Terminal
./producer_consumer

Step 4: Expected Output

Producer: produced item 1 at index 0 Producer: produced item 2 at index 1 Consumer: consumed item 1 from index 0 Producer: produced item 3 at index 2 Consumer: consumed item 2 from index 1 Producer: produced item 4 at index 3 Consumer: consumed item 3 from index 2 Producer: produced item 5 at index 4 Consumer: consumed item 4 from index 3 Producer: produced item 6 at index 0 Consumer: consumed item 5 from index 4 Producer: produced item 7 at index 1 Consumer: consumed item 6 from index 0 Producer: produced item 8 at index 2 Consumer: consumed item 7 from index 1 Producer: produced item 9 at index 3 Consumer: consumed item 8 from index 2 Producer: produced item 10 at index 4 Consumer: consumed item 9 from index 3 Consumer: consumed item 10 from index 4 All items produced and consumed successfully!

๐Ÿ” What to observe: The buffer index wraps around (circular buffer). Producer never overflows. Consumer never reads empty. This is synchronization at work!

๐ŸŸก Tier 2 โ€” SEMI-GUIDED: Race Condition Demo + Mutex Fix

โฑ๏ธ 45โ€“60 minutesIntermediateHints provided, you fill the gaps

Your Mission:

Demonstrate a race condition by having two threads increment a shared counter 1,000,000 times each WITHOUT a lock. Then fix it with a mutex.

Hints:

  1. Step 1: Create race_demo.c. Declare a global int counter = 0;
  2. Step 2: Write a function void* increment(void* arg) that loops 1,000,000 times doing counter++
  3. Step 3: In main(), create 2 threads running increment, join both, then print counter
  4. Run multiple times: You'll see counter is NOT 2,000,000! It varies each run (race condition!)
  5. Fix: Add pthread_mutex_t lock, wrap counter++ with pthread_mutex_lock() and pthread_mutex_unlock()
  6. Run again: Counter is now exactly 2,000,000 every time โœ…
Stretch Goal: Time both versions using clock_gettime(). How much slower is the mutex version? Why? Is there a way to reduce lock contention?

๐Ÿ”ด Tier 3 โ€” OPEN CHALLENGE: Dining Philosophers with Deadlock Detection

โฑ๏ธ 2โ€“3 hoursAdvancedNo instructions โ€” real systems programming

The Brief:

Implement the full Dining Philosophers problem in C with pthreads. Requirements:

  1. 5 philosopher threads, 5 chopstick mutexes
  2. Implement the naรฏve solution first (left then right) โ€” observe deadlock
  3. Add deadlock detection: a monitor thread that periodically checks if all philosophers are holding one chopstick and waiting for another
  4. Implement the asymmetric solution (even/odd) to prevent deadlock
  5. Print state transitions: THINKING โ†’ HUNGRY โ†’ EATING
  6. Run for 30 seconds and report statistics: how many times each philosopher ate

Deliverable: Two C files โ€” dining_deadlock.c (with deadlock) and dining_safe.c (deadlock-free). Compare outputs.

This is a classic interview question at Amazon, Google, and Flipkart for backend/systems roles. Having working code for Dining Philosophers on your GitHub = instant credibility.
Section E

Industry Spotlight โ€” A Day in the Life

๐Ÿ‘ฉโ€๐Ÿ’ป Aisha Begum, 28 โ€” Backend Engineer at Razorpay, Bangalore

Background: B.Tech CSE from JNTU Hyderabad. Self-taught competitive programming in 2nd year. Interned at a Hyderabad startup building payment APIs. Got placed at Razorpay through an off-campus drive after solving concurrency-related system design questions.

A Typical Day:

9:30 AM โ€” Morning standup with the Payments Platform team. Review overnight alerts โ€” any payment failures due to race conditions or timeouts?

10:00 AM โ€” Investigate a bug: two merchants received the same transaction ID. Root cause? A UUID generation function wasn't thread-safe. Fix: wrap it with a mutex.

11:30 AM โ€” Code review for a junior developer's implementation of a rate limiter. "Your counter increment isn't atomic โ€” use CAS or a semaphore here."

1:00 PM โ€” Lunch at Razorpay's cafeteria. Discuss the upcoming migration from mutex-based locks to lock-free queues for the payment processing pipeline.

2:00 PM โ€” Write a Go implementation of a concurrent worker pool using channels (Go's version of monitors). Each worker processes payment webhooks.

4:00 PM โ€” Load testing: simulate 10,000 concurrent payment requests. Check for race conditions using Go's race detector (go run -race).

5:30 PM โ€” Write documentation for the team's concurrency best practices guide.

DetailInfo
Tools Used DailyGo, Redis (distributed locks), PostgreSQL, Kafka, Docker, Git
Entry Salary (2024)โ‚น8โ€“12 LPA + ESOPs
Mid-Level (3โ€“5 yrs)โ‚น15โ€“25 LPA
Senior (7+ yrs)โ‚น30โ€“55 LPA
Companies HiringRazorpay, PhonePe, Paytm, Zerodha, CRED, Flipkart, Amazon, Google, Atlassian, Swiggy
Section F

Earn With It โ€” Concurrency Bug Detection Consulting

๐Ÿ’ฐ Your Earning Path After This Chapter

Portfolio Piece: "Thread-Safe Producer-Consumer System in C" + "Dining Philosophers with Deadlock Detection" โ€” working GitHub repos with README, compilation instructions, and output screenshots.

Freelance & Consulting Gig Ideas:

โ€ข Concurrency bug detection in existing codebases โ€” โ‚น5,000โ€“โ‚น15,000/project

โ€ข Thread-safe API wrapper development for startups โ€” โ‚น8,000โ€“โ‚น20,000

โ€ข Code review for race conditions in fintech applications โ€” โ‚น3,000โ€“โ‚น10,000/review

โ€ข OS/Systems Programming tutoring for GATE/placement students โ€” โ‚น500โ€“โ‚น1,000/hour

PlatformBest ForTypical Rate
Topmate1-on-1 OS/Systems mentoringโ‚น500โ€“โ‚น2,000/session
UpworkConcurrency bug fixing, C/Go projects$20โ€“$60/hour
GitHub SponsorsOpen-source sync librariesVariable
InternshalaSystems programming internshipsโ‚น5,000โ€“โ‚น15,000/month
College NetworkGATE OS tutoringโ‚น500โ€“โ‚น1,500/hour

โฑ๏ธ Time to First Earning: 2โ€“4 weeks (complete labs, push to GitHub, start tutoring GATE aspirants on concurrency topics)

GATE CS has 5โ€“8 marks on process synchronization every year. If you can solve Dining Philosophers and Producer-Consumer problems confidently, you can tutor other students for โ‚น500โ€“โ‚น1,000/hour. Start with your own college batch!
Section G

MCQ Assessment Bank โ€” 30 Questions (Bloom's Mapped)

Remember / Identify (Q1โ€“Q5)

Q1

Which of the following is NOT a requirement of the critical section problem?

  1. Mutual Exclusion
  2. Progress
  3. Starvation Freedom
  4. Bounded Waiting
Remember
โœ… Answer: (C) Starvation Freedom โ€” The three requirements are Mutual Exclusion, Progress, and Bounded Waiting. Starvation freedom is a related but separate property not listed as one of the three requirements.
Q2

In Peterson's Algorithm, what is the purpose of the turn variable?

  1. To count the number of processes
  2. To act as a tie-breaker when both processes want to enter
  3. To store the process ID
  4. To measure execution time
Remember
โœ… Answer: (B) โ€” The turn variable breaks ties when both processes set their flags to true simultaneously. Whichever process set turn last gives priority to the other.
Q3

A binary semaphore can take values:

  1. 0 and 1 only
  2. Any positive integer
  3. -1 and 1
  4. 0 to N
Remember
โœ… Answer: (A) โ€” A binary semaphore is restricted to 0 and 1, making it equivalent to a mutex lock for mutual exclusion.
Q4

The P() operation on a semaphore is also known as:

  1. signal()
  2. wait()
  3. notify()
  4. release()
Remember
โœ… Answer: (B) โ€” P() = wait() = down(). It decrements the semaphore (or blocks if semaphore is 0). Named from Dutch "proberen" (to test).
Q5

In the Dining Philosophers problem, how many chopsticks are shared among 5 philosophers?

  1. 10
  2. 5
  3. 4
  4. 3
Remember
โœ… Answer: (B) โ€” There are 5 chopsticks, one between each pair of adjacent philosophers around the circular table.

Understand / Explain (Q6โ€“Q10)

Q6

Why does the naรฏve Dining Philosophers solution cause deadlock?

  1. Because philosophers eat too fast
  2. Because all philosophers may pick up their left chopstick simultaneously, creating circular wait
  3. Because there aren't enough chopsticks
  4. Because the semaphores are initialized incorrectly
Understand
โœ… Answer: (B) โ€” If all 5 philosophers pick up their left chopstick at the same time, each holds one chopstick and waits for their right โ€” creating a circular wait (one of the four necessary conditions for deadlock).
Q7

What is the key difference between a monitor's signal() and a semaphore's V()?

  1. signal() increments a counter; V() does not
  2. V() always increments the semaphore; signal() has no effect if no process is waiting
  3. They are identical operations
  4. signal() can wake multiple processes; V() wakes only one
Understand
โœ… Answer: (B) โ€” A semaphore's V() always increments the counter (the signal is "remembered"). A monitor's signal() is lost if no process is waiting on the condition variable โ€” it does not accumulate.
Q8

In the Reader-Writer problem with reader preference, what can happen to writers?

  1. Writers get priority over readers
  2. Writers may starve if readers keep arriving continuously
  3. Writers and readers get equal access
  4. Writers can always interrupt readers
Understand
โœ… Answer: (B) โ€” In the reader-preference solution, as long as at least one reader is active and new readers keep arriving, the writer is locked out indefinitely โ€” this is writer starvation.
Q9

Why can't user-level threads take advantage of multiple CPU cores?

  1. User-level threads are too slow
  2. The kernel sees the entire process as a single schedulable unit
  3. User-level threads don't support mutexes
  4. User-level threads can only run on core 0
Understand
โœ… Answer: (B) โ€” The kernel is unaware of user-level threads. It schedules the process as a whole on one core. Since thread management is entirely in user space, the OS cannot assign different threads to different CPUs.
Q10

What is the purpose of the mutex semaphore in the Producer-Consumer solution?

  1. To count the number of items in the buffer
  2. To ensure mutual exclusion when accessing the shared buffer
  3. To track which slot is empty
  4. To wake up sleeping processes
Understand
โœ… Answer: (B) โ€” The mutex semaphore (initialized to 1) ensures that only one process (producer or consumer) accesses the buffer at a time, preventing data corruption.

Apply / Solve (Q11โ€“Q15)

Q11

In Peterson's Algorithm, if process Pโ‚€ sets flag[0]=true and turn=1, and Pโ‚ has flag[1]=false, what happens?

  1. Pโ‚€ enters the critical section immediately
  2. Pโ‚€ waits indefinitely
  3. Both processes enter the critical section
  4. Deadlock occurs
Apply
โœ… Answer: (A) โ€” Pโ‚€'s while condition checks flag[1] && turn==1. Since flag[1] is false, the condition is false, and Pโ‚€ enters immediately. Pโ‚ doesn't want to enter.
Q12

A counting semaphore is initialized to 3. After the sequence P(), P(), P(), V(), P(), what is the semaphore value?

  1. 0
  2. 1
  3. -1
  4. 2
Apply
โœ… Answer: (A) โ€” Starting at 3: P()โ†’2, P()โ†’1, P()โ†’0, V()โ†’1, P()โ†’0. The value is 0.
Q13

In the bounded buffer problem with buffer size 5, if 5 items are produced and 2 are consumed, what are the values of semaphores empty and full?

  1. empty=2, full=3
  2. empty=0, full=5
  3. empty=3, full=2
  4. empty=5, full=0
Apply
โœ… Answer: (A) โ€” Initially empty=5, full=0. After 5 produces: empty=0, full=5. After 2 consumes: empty=2, full=3.
Q14

What is the output of test_and_set(&lock) when lock is currently false?

  1. Returns true, lock remains false
  2. Returns false, lock becomes true
  3. Returns true, lock becomes true
  4. Returns false, lock remains false
Apply
โœ… Answer: (B) โ€” TAS saves the old value (false), sets lock to true, and returns the old value (false). The caller exits the while loop and enters the critical section.
Q15

In the Reader-Writer solution, when the 1st reader calls P(rw_mutex), what happens to incoming writers?

  1. Writers can still enter
  2. Writers are blocked until all readers finish
  3. Writers terminate
  4. Nothing โ€” rw_mutex is not affected
Apply
โœ… Answer: (B) โ€” The first reader acquires rw_mutex, blocking writers. Subsequent readers increment read_count without acquiring rw_mutex again. Only when the LAST reader leaves (read_count becomes 0) is rw_mutex released, allowing writers.

Analyze / Compare (Q16โ€“Q20)

Q16

Two threads execute counter++ 100 times each on a shared variable counter=0. What is the MINIMUM possible value of counter?

  1. 200
  2. 100
  3. 2
  4. 0
Analyze
โœ… Answer: (C) โ€” In the worst case, both threads read counter=0, increment to 1, and write 1 back โ€” repeatedly. The minimum possible is 2 (each thread's last write must persist at least once). This requires careful interleaving analysis.
Q17

Which synchronization primitive uses busy waiting?

  1. Semaphore (with blocking)
  2. Monitor
  3. Test-and-Set spinlock
  4. Condition variable
Analyze
โœ… Answer: (C) โ€” A TAS spinlock uses a while loop that continuously checks the lock variable, consuming CPU cycles (busy waiting). Semaphores with blocking and monitors put the process to sleep instead.
Q18

What happens if a producer calls P(mutex) before P(empty) in the bounded buffer problem and the buffer is full?

  1. The producer waits efficiently
  2. Deadlock โ€” producer holds mutex and blocks on empty; consumer needs mutex to free a slot
  3. The producer overwrites the buffer
  4. Nothing unusual happens
Analyze
โœ… Answer: (B) โ€” The producer acquires mutex, then blocks on P(empty) because buffer is full. The consumer tries P(full) (succeeds, there are items) but then P(mutex) blocks because the producer holds it. Neither can proceed โ€” classic deadlock!
Q19

Compare: In Many-to-One threading model, what happens when one thread makes a blocking system call?

  1. Only that thread blocks
  2. All threads in the process block
  3. The kernel creates a new thread
  4. The system call is converted to non-blocking
Analyze
โœ… Answer: (B) โ€” In Many-to-One, all user threads map to one kernel thread. When the kernel thread blocks (due to I/O, etc.), ALL user threads are effectively blocked since the kernel sees only one thread.
Q20

Analyzing the Bakery Algorithm: if two processes get the same ticket number, how is the tie resolved?

  1. Random selection
  2. The process with the smaller process ID goes first
  3. Both enter the critical section
  4. Both are denied entry
Analyze
โœ… Answer: (B) โ€” The comparison (number[j], j) < (number[i], i) uses process ID as a tie-breaker. The process with the smaller ID (lower j) gets priority when ticket numbers are equal.

Evaluate / Judge (Q21โ€“Q25)

Q21

For a payment gateway processing 10,000 concurrent transactions, which is the BEST synchronization approach?

  1. Peterson's Algorithm
  2. Disabling interrupts
  3. Mutex locks with lock-free queues for high-throughput paths
  4. Test-and-Set spinlocks on all operations
Evaluate
โœ… Answer: (C) โ€” Peterson's only works for 2 processes. Disabling interrupts doesn't work on multiprocessors. Spinlocks waste CPU. Mutex + lock-free structures (using CAS) provide both correctness and high throughput for real payment systems like Razorpay.
Q22

A system needs multiple readers but exclusive writers on a shared database. Which solution risks writer starvation?

  1. Writer-preference Reader-Writer
  2. Reader-preference Reader-Writer
  3. Binary semaphore for all operations
  4. Monitor with fair scheduling
Evaluate
โœ… Answer: (B) โ€” Reader-preference allows unlimited concurrent readers. If readers continuously arrive, the writer never gets access (starvation). Writer-preference solves this by blocking new readers once a writer is waiting.
Q23

Which threading model would you recommend for a web server handling thousands of concurrent connections on a multi-core machine?

  1. Many-to-One
  2. One-to-One
  3. Many-to-Many
  4. Single-threaded event loop only
Evaluate
โœ… Answer: (C) โ€” Many-to-Many gives the best balance: it can handle many user-level threads efficiently while exploiting multiple CPU cores through kernel threads. One-to-One works but may hit kernel thread limits. Many-to-One can't use multiple cores.
Q24

Evaluate: Is Peterson's Algorithm suitable for modern multicore x86 processors without modifications?

  1. Yes, it works perfectly on all hardware
  2. No, because out-of-order execution and CPU cache coherency can violate its assumptions
  3. Yes, but only on Intel processors
  4. No, because it requires more than 2 processes
Evaluate
โœ… Answer: (B) โ€” Modern CPUs reorder memory operations for performance. Peterson's assumes sequential consistency (reads/writes happen in program order). Without memory barriers (fence instructions), the algorithm may fail on multicore systems.
Q25

A startup asks: "Should we use spinlocks or blocking mutexes for protecting a shared counter updated every 10 microseconds?" What's your advice?

  1. Spinlocks โ€” the critical section is very short, so spinning is cheaper than context switching
  2. Blocking mutexes โ€” always better than spinlocks
  3. Disable interrupts โ€” simplest approach
  4. No synchronization needed โ€” 10ฮผs is too fast for conflicts
Evaluate
โœ… Answer: (A) โ€” When the critical section is very short (microseconds), spinning wastes less time than the overhead of putting a thread to sleep and waking it up (context switch costs ~1-10ฮผs). Spinlocks are ideal for short critical sections on multicore systems.

Create / Design (Q26โ€“Q30)

Q26

You're designing a thread-safe queue for a message broker. Which combination of synchronization primitives would you use?

  1. One binary semaphore only
  2. Mutex for buffer access + counting semaphore for item count + counting semaphore for free space
  3. Two Peterson's Algorithms
  4. Global interrupt disable
Create
โœ… Answer: (B) โ€” This is the classic bounded-buffer pattern. Mutex ensures exclusive access, a "full" counting semaphore tracks available items, and an "empty" counting semaphore tracks free slots. This is how Kafka and RabbitMQ implement internal queues.
Q27

To implement a precedence constraint where S3 must execute after both S1 and S2, which semaphore setup is correct?

  1. One semaphore initialized to 2; S1 does V(), S2 does V(), S3 does P() twice
  2. One semaphore initialized to 0; S1 does V(), S3 does P()
  3. Two semaphores a=0, b=0; S1 does V(a), S2 does V(b), S3 does P(a) then P(b)
  4. No semaphores needed
Create
โœ… Answer: (C) โ€” Two separate semaphores ensure S3 waits for BOTH S1 and S2. Using one semaphore with initial value 2 and two P() calls would also work (option A), but option C is more precise and standard.
Q28

You need to design a system where exactly 3 threads can access a resource simultaneously. Which construct do you use?

  1. Binary semaphore
  2. Counting semaphore initialized to 3
  3. 3 separate mutex locks
  4. Peterson's Algorithm
Create
โœ… Answer: (B) โ€” A counting semaphore initialized to 3 allows exactly 3 threads to pass P() before subsequent threads must wait. Each V() frees one slot. Binary semaphore only allows 1; Peterson's only handles 2 processes.
Q29

Design question: In a ride-sharing app, multiple users request the same driver simultaneously. How would you ensure only ONE user gets matched?

  1. Use a counting semaphore initialized to the number of drivers
  2. Use Compare-and-Swap (CAS) on the driver's availability flag
  3. Let all users book and cancel extras later
  4. Use a Many-to-One threading model
Create
โœ… Answer: (B) โ€” CAS atomically checks if the driver is available (expected=AVAILABLE) and sets it to BOOKED. Only ONE thread's CAS will succeed; others will fail and move to the next driver. This is how Uber and Ola handle driver matching.
Q30

You're building an IRCTC-like system where 500 users try to book 1 seat. Design the synchronization:

  1. Use a monitor with a condition variable: check seat count, if > 0 decrement and confirm, else wait
  2. Use 500 separate mutex locks
  3. No synchronization โ€” first-come-first-served is automatic
  4. Use Peterson's Algorithm for all 500 users
Create
โœ… Answer: (A) โ€” A monitor ensures mutual exclusion automatically. Inside the monitor, check if seats > 0. If yes, decrement and confirm booking. If no, the user gets a "sold out" message. The condition variable can be used to notify waiting users if a cancellation occurs. Peterson's only works for 2 processes.
Section H

Short Answer Questions (5)

๐Ÿ“ Q1: Explain the three requirements of the Critical Section Problem with a real-world analogy.

Answer:

1. Mutual Exclusion: Only one process can be in its critical section at a time. Analogy: Only one person can use the ATM booth โ€” the door locks from inside.

2. Progress: If no process is in the critical section and some want to enter, the decision of who enters cannot be postponed indefinitely. Only processes not in their remainder section participate in the decision. Analogy: If the ATM is free and people are waiting, one of them must be allowed in โ€” you can't keep the booth empty.

3. Bounded Waiting: There must be a bound on the number of times other processes can enter before a waiting process gets its turn. Analogy: In a bank queue with token numbers, you're guaranteed to be served after a finite number of customers โ€” nobody can keep cutting the line.

๐Ÿ“ Q2: Differentiate between binary semaphore and counting semaphore with examples.

Answer:

Binary Semaphore (Mutex): Takes values 0 or 1 only. Used for mutual exclusion โ€” ensuring one process at a time. Example: A single-key bathroom โ€” key is either with someone (0) or hanging on the hook (1).

Counting Semaphore: Takes values from 0 to N. Used to control access to a resource with multiple instances. Example: A parking lot with 50 spaces โ€” semaphore starts at 50, each car entering decrements it, each car leaving increments it. When 0, new cars must wait.

Key difference: Binary semaphore = one resource. Counting semaphore = N identical resources.

๐Ÿ“ Q3: Why does the naรฏve Dining Philosophers solution lead to deadlock? How can you fix it?

Answer:

Deadlock cause: Each philosopher picks up their left chopstick first. If all 5 do this simultaneously, each holds one chopstick and waits for the other โ€” creating a circular wait. All four conditions for deadlock are met: mutual exclusion (chopstick is exclusive), hold and wait (holding left, waiting for right), no preemption (can't force a philosopher to release), and circular wait (P0โ†’P1โ†’P2โ†’P3โ†’P4โ†’P0).

Solutions:

  • Asymmetric ordering: Even-numbered philosophers pick left first, odd pick right first โ€” breaks circular wait
  • Limit to 4: Allow at most 4 philosophers to sit simultaneously (counting semaphore = 4)
  • All-or-nothing: Pick up both chopsticks only if both are available, inside a critical section

๐Ÿ“ Q4: Compare user-level threads and kernel-level threads. When would you choose each?

Answer:

User-Level Threads: Managed by a thread library in user space. Faster context switching (no kernel involvement), portable across OSes. BUT: if one thread blocks, all block; cannot use multiple CPUs.

Kernel-Level Threads: Managed by the OS kernel. Can exploit multiple CPUs, one blocking thread doesn't block others. BUT: slower creation and context switching due to system calls.

Choose ULT when: You need many lightweight threads with fast switching and the application is CPU-bound (no blocking I/O). Example: cooperative multitasking in game engines.

Choose KLT when: You need true parallelism on multicore CPUs, or your application does blocking I/O (network servers, file processing). Example: Web servers handling concurrent HTTP requests.

๐Ÿ“ Q5: What is a precedence graph? Draw one for: S1โ†’S2, S1โ†’S3, S2โ†’S4, S3โ†’S4. Implement with semaphores.

Answer:

A precedence graph is a DAG (Directed Acyclic Graph) showing execution order constraints between statements. An edge from Si to Sj means Si must complete before Sj begins.

Graph
       S1
      /  \
    S2    S3
      \  /
       S4

S2 and S3 can execute in parallel (both depend only on S1). S4 must wait for both S2 and S3.

Semaphore implementation:

Pseudocode
semaphore a=0, b=0, c=0, d=0;
P1: S1; V(a); V(b);
P2: P(a); S2; V(c);
P3: P(b); S3; V(d);
P4: P(c); P(d); S4;
Section I

Case Studies (2)

๐Ÿ“‹ Case Study 1: BookMyShow โ€” The Race Condition That Oversold 200 Tickets

Background:

In 2019, during the release of Avengers: Endgame in India, BookMyShow experienced its highest-ever traffic โ€” 18 million searches per hour. Multiple users across cities were booking the last few seats in premium theatres simultaneously.

The Problem:

The booking system's "check availability โ†’ deduct seat โ†’ confirm" logic was not atomic. The code looked approximately like this:

Pseudocode (Buggy)
book_seat(theatre_id, seat_id):
    available = db.query("SELECT available FROM seats WHERE id = ?", seat_id)
    if available > 0:
        db.update("UPDATE seats SET available = available - 1 WHERE id = ?", seat_id)
        return "BOOKING CONFIRMED"
    else:
        return "SOLD OUT"

Between the SELECT and UPDATE, another thread could read the same available count โ€” classic TOCTOU (Time-of-Check-to-Time-of-Use) race condition.

Impact:

~200 tickets were double-booked across 15 cities. Customers arrived at theatres to find their seats occupied. Result: โ‚น3 lakh in refunds + compensation + massive social media backlash.

Solution Applied:

Pseudocode (Fixed)
book_seat(theatre_id, seat_id):
    // Atomic operation โ€” UPDATE only succeeds if available > 0
    rows_affected = db.update(
        "UPDATE seats SET available = available - 1 WHERE id = ? AND available > 0",
        seat_id
    )
    if rows_affected == 1:
        return "BOOKING CONFIRMED"
    else:
        return "SOLD OUT"

Additionally, they implemented:

  • Distributed locks (Redis-based mutex): For high-demand shows, a distributed lock prevents concurrent booking of the same seat
  • Optimistic locking with version numbers: Each seat row has a version column; CAS-style update
  • Queue-based booking: For Tatkal-style launches, users are placed in a virtual queue (counting semaphore pattern)

Discussion Questions:

  1. Why is the fixed SQL query better than wrapping the original code with a mutex?
  2. How is the virtual queue system similar to a counting semaphore?
  3. What would happen if BookMyShow used Peterson's Algorithm for synchronization?

๐Ÿ“‹ Case Study 2: Indian Railways Signal System โ€” Semaphores in Real Life

Background:

Indian Railways operates 13,000+ trains daily on 67,000+ km of track. Many sections are single-track where trains going in opposite directions share the same rail. This is literally the "single-lane bridge" analogy from the Critical Section Problem!

The Synchronization Challenge:

A single-track section between two stations is a critical section. Only one train can occupy it at a time. The railway signal system acts as a semaphore:

Railway ConceptOS Concept
Single-track sectionCritical Section (shared resource)
Green signalP() succeeds โ€” semaphore > 0, enter CS
Red signalP() blocks โ€” semaphore = 0, wait
Train exits sectionV() โ€” signal, release the section
Token/Tablet systemBinary semaphore (mutex)
Double-track sectionCounting semaphore (2 trains, one per direction)

The Token System (Physical Mutex!):

In traditional Indian Railways, single-track sections use a physical token system. A metal token is kept at the station. A train driver MUST hold the token to enter the section. Only ONE token exists per section. When the train exits, the token is returned. This is a binary semaphore implemented in hardware โ€” invented before computers!

Modern Electronic Interlocking:

Today, most Indian Railway stations use Electronic Interlocking (EI) systems. These are computerised systems that implement the same logic digitally:

  • Mutual exclusion: Only one train route set at a time per track section
  • Progress: If the section is free, a waiting train is allowed immediately
  • Bounded waiting: Priority rules ensure no train waits indefinitely

The 2023 Balasore Accident Connection:

The tragic Odisha train accident (June 2023, 296 deaths) was partly attributed to errors in the electronic interlocking system during maintenance. A signalling configuration error sent the Coromandel Express onto the wrong track. This is analogous to a race condition in the synchronization system โ€” the interlocking failed to ensure mutual exclusion on the track section.

Discussion Questions:

  1. How is the token system analogous to a binary semaphore? What is P() and V() in this context?
  2. If Indian Railways converted a single-track section to double-track, how would the semaphore value change?
  3. What synchronization failures could explain the Balasore accident from an OS perspective?
Section J

Chapter Summary

๐Ÿง  Key Takeaways โ€” Unit 4

  • Race Condition: When multiple processes access shared data concurrently and the outcome depends on the order of execution
  • Critical Section Problem: Requires Mutual Exclusion, Progress, and Bounded Waiting
  • Peterson's Algorithm: Software solution for 2 processes using flag[] + turn
  • Bakery Algorithm: Software solution for N processes using ticket numbers
  • TAS & CAS: Hardware atomic instructions for building locks; TAS returns old value and sets to true; CAS only swaps if current value matches expected
  • Semaphores: P() = wait/decrement; V() = signal/increment. Binary (0/1) for mutex, Counting (0โ€“N) for resource pools
  • Monitors: High-level construct with automatic mutual exclusion; condition variables with wait() and signal()
  • Producer-Consumer: 3 semaphores โ€” mutex, empty, full. ALWAYS acquire counting semaphore before mutex!
  • Reader-Writer: Reader preference may starve writers; writer preference may starve readers
  • Dining Philosophers: Naรฏve solution deadlocks; fix with asymmetric ordering, limiting to N-1, or all-or-nothing
  • Precedence Graphs: DAGs showing execution order; implemented with semaphores (one per edge)
  • Threads: Lightweight processes sharing code/data/files. User-level (fast, no multicore) vs Kernel-level (slower, multicore)
  • Threading Models: Many-to-One (no parallelism), One-to-One (true parallelism, overhead), Many-to-Many (best of both)
  • Pthreads: POSIX API โ€” pthread_create(), pthread_join(), pthread_mutex_lock/unlock()
Section K

Earning Checkpoint โ€” Skills Inventory After Unit 4

Skill AcquiredTool/TechniquePortfolio DeliverableEarning-Ready?
Critical Section & Synchronization TheoryConceptualโ€”โœ… Yes โ€” GATE/Interview ready
Semaphore ProgrammingPOSIX semaphores in CProducer-Consumer programโœ… Yes โ€” systems programming roles
Mutex & Thread-Safe CodePthreads APIRace condition demo + fixโœ… Yes โ€” backend engineering roles
Classical ProblemsDining Philosophers, Reader-WriterWorking implementations on GitHubโœ… Yes โ€” interview differentiator
Concurrency Bug DetectionCode review, race analysisBug detection case studiesโœ… Yes โ€” โ‚น5Kโ€“โ‚น15K/project consulting
OS Tutoring (GATE prep)Synchronization conceptsTeaching notes + solved MCQsโœ… Yes โ€” โ‚น500โ€“โ‚น1,000/hour
Minimum Viable Earning Setup after this chapter: A GitHub portfolio with Producer-Consumer + Dining Philosophers code + a Topmate profile offering OS tutoring = you can earn โ‚น8,000โ€“โ‚น25,000/month from tutoring + freelance code review while still in college.

โœ… Unit 4 complete. MCQs: 30. Ready for Unit 5: Deadlock!

[QR: Link to EduArtha video tutorial โ€” Process Synchronization & Threads]