Multi-threading and Concurrency Patterns

1. Producer-Consumer Pattern (Concurrency)

Component Purpose Synchronization Key Insight
Shared Buffer Queue to hold items between producer/consumer Mutex lock for thread safety Bounded buffer prevents overflow
Producer Creates items, adds to buffer Wait when buffer is full Signal consumer when item added
Consumer Removes items from buffer, processes them Wait when buffer is empty Signal producer when item removed
Condition Variables notFull, notEmpty signals Coordinate producer/consumer Avoid busy waiting

Example: Producer-Consumer with bounded buffer

// JavaScript simulation using Promises and async/await
class BoundedBuffer {
    constructor(capacity) {
        this.capacity = capacity;
        this.buffer = [];
        this.mutex = new Promise(resolve => resolve());
        this.notFull = [];   // Wait queue for producers
        this.notEmpty = [];  // Wait queue for consumers
    }
    
    async produce(item) {
        // Acquire lock
        await this.mutex;
        
        // Wait while buffer is full
        while (this.buffer.length >= this.capacity) {
            await new Promise(resolve => this.notFull.push(resolve));
        }
        
        // Add item to buffer
        this.buffer.push(item);
        console.log(`Produced: ${item}, Buffer: [${this.buffer}]`);
        
        // Signal consumer that buffer is not empty
        if (this.notEmpty.length > 0) {
            const resolve = this.notEmpty.shift();
            resolve();
        }
    }
    
    async consume() {
        // Acquire lock
        await this.mutex;
        
        // Wait while buffer is empty
        while (this.buffer.length === 0) {
            await new Promise(resolve => this.notEmpty.push(resolve));
        }
        
        // Remove item from buffer
        const item = this.buffer.shift();
        console.log(`Consumed: ${item}, Buffer: [${this.buffer}]`);
        
        // Signal producer that buffer is not full
        if (this.notFull.length > 0) {
            const resolve = this.notFull.shift();
            resolve();
        }
        
        return item;
    }
}

// Conceptual implementation showing the pattern
class ProducerConsumer {
    constructor(bufferSize) {
        this.buffer = [];
        this.capacity = bufferSize;
    }
    
    produce(item) {
        while (this.buffer.length >= this.capacity) {
            // Wait (in real threading: condition variable wait)
        }
        
        this.buffer.push(item);
        // Signal consumers (notEmpty.notify())
    }
    
    consume() {
        while (this.buffer.length === 0) {
            // Wait (in real threading: condition variable wait)
        }
        
        const item = this.buffer.shift();
        // Signal producers (notFull.notify())
        return item;
    }
}

// Usage pattern:
// Producer thread:
//   while (true) {
//     item = produceItem()
//     buffer.produce(item)
//   }
//
// Consumer thread:
//   while (true) {
//     item = buffer.consume()
//     processItem(item)
//   }
Note: Producer-Consumer pattern decouples production from consumption. Bounded buffer prevents producer from overwhelming consumer. Critical: lock before checking condition, wait atomically releases lock and sleeps.

2. Print in Order Problem (Thread Synchronization)

Approach Mechanism Complexity Use Case
Semaphores Binary semaphores, initial count controls order O(1) time, O(1) space Simple ordering
Condition Variables Wait/notify on shared counter O(1) time, O(1) space Complex predicates
Locks with Flags Boolean flags + busy waiting or sleep O(1) space, inefficient Not recommended
Promises/Futures Chain promises for sequential execution JavaScript pattern Async coordination

Example: Print in order using semaphores

// Conceptual semaphore implementation
class Semaphore {
    constructor(count) {
        this.count = count;
        this.waitQueue = [];
    }
    
    async acquire() {
        if (this.count > 0) {
            this.count--;
        } else {
            await new Promise(resolve => this.waitQueue.push(resolve));
        }
    }
    
    release() {
        if (this.waitQueue.length > 0) {
            const resolve = this.waitQueue.shift();
            resolve();
        } else {
            this.count++;
        }
    }
}

class Foo {
    constructor() {
        this.sem1 = new Semaphore(0); // Initially locked
        this.sem2 = new Semaphore(0); // Initially locked
    }
    
    async first(printFirst) {
        printFirst(); // Prints "first"
        this.sem1.release(); // Allow second to run
    }
    
    async second(printSecond) {
        await this.sem1.acquire(); // Wait for first
        printSecond(); // Prints "second"
        this.sem2.release(); // Allow third to run
    }
    
    async third(printThird) {
        await this.sem2.acquire(); // Wait for second
        printThird(); // Prints "third"
    }
}

// Usage: Even if threads start in any order,
// output is always "firstsecondthird"
const foo = new Foo();
// Thread A: foo.third(() => console.log('third'))
// Thread B: foo.first(() => console.log('first'))
// Thread C: foo.second(() => console.log('second'))
// Output: "firstsecondthird"

Example: Using promises in JavaScript

class FooPromises {
    constructor() {
        this.promise1 = new Promise(resolve => {
            this.resolve1 = resolve;
        });
        this.promise2 = new Promise(resolve => {
            this.resolve2 = resolve;
        });
    }
    
    async first(printFirst) {
        printFirst();
        this.resolve1(); // Unblock second
    }
    
    async second(printSecond) {
        await this.promise1; // Wait for first
        printSecond();
        this.resolve2(); // Unblock third
    }
    
    async third(printThird) {
        await this.promise2; // Wait for second
        printThird();
    }
}

// Alternative: using counter and condition
class FooCounter {
    constructor() {
        this.counter = 0;
    }
    
    async first(printFirst) {
        printFirst();
        this.counter = 1;
    }
    
    async second(printSecond) {
        while (this.counter !== 1) {
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        printSecond();
        this.counter = 2;
    }
    
    async third(printThird) {
        while (this.counter !== 2) {
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        printThird();
    }
}

3. Dining Philosophers Problem

Solution Strategy Deadlock-Free Trade-off
Resource Ordering Always pick lower-numbered fork first ✓ Yes Simple, breaks circular wait
Waiter Solution Semaphore limits concurrent diners to N-1 ✓ Yes Reduces parallelism
Chandy/Misra Message passing with dirty/clean forks ✓ Yes Complex, distributed systems
Timeout Release forks if can't acquire both ✓ Livelock possible May waste CPU cycles

Example: Dining philosophers with resource ordering

// 5 philosophers, 5 forks
// Deadlock occurs if each philosopher picks left fork simultaneously
// Solution: Pick lower-numbered fork first

class DiningPhilosophers {
    constructor() {
        this.forks = [
            { locked: false, id: 0 },
            { locked: false, id: 1 },
            { locked: false, id: 2 },
            { locked: false, id: 3 },
            { locked: false, id: 4 }
        ];
    }
    
    async wantsToEat(
        philosopher,
        pickLeftFork,
        pickRightFork,
        eat,
        putLeftFork,
        putRightFork
    ) {
        const leftFork = philosopher;
        const rightFork = (philosopher + 1) % 5;
        
        // Resource ordering: always pick lower-numbered fork first
        const firstFork = Math.min(leftFork, rightFork);
        const secondFork = Math.max(leftFork, rightFork);
        
        // Acquire first fork
        await this.acquireFork(firstFork);
        if (firstFork === leftFork) pickLeftFork();
        else pickRightFork();
        
        // Acquire second fork
        await this.acquireFork(secondFork);
        if (secondFork === leftFork) pickLeftFork();
        else pickRightFork();
        
        // Eat
        eat();
        
        // Release forks
        this.releaseFork(firstFork);
        if (firstFork === leftFork) putLeftFork();
        else putRightFork();
        
        this.releaseFork(secondFork);
        if (secondFork === leftFork) putLeftFork();
        else putRightFork();
    }
    
    async acquireFork(forkId) {
        while (this.forks[forkId].locked) {
            await new Promise(resolve => setTimeout(resolve, 10));
        }
        this.forks[forkId].locked = true;
    }
    
    releaseFork(forkId) {
        this.forks[forkId].locked = false;
    }
}

// Why resource ordering prevents deadlock:
// Philosopher 0: picks fork 0, then fork 1
// Philosopher 1: picks fork 1, then fork 2
// Philosopher 2: picks fork 2, then fork 3
// Philosopher 3: picks fork 3, then fork 4
// Philosopher 4: picks fork 0 (min), then fork 4 (max)
//
// Circular wait broken: Philosopher 4 competes with 0 for fork 0
// At least one philosopher can always acquire both forks

Example: Waiter solution (limit concurrent diners)

class DiningPhilosophersWaiter {
    constructor() {
        this.forks = Array(5).fill(false); // false = available
        this.waiter = new Semaphore(4); // Allow max 4 philosophers
    }
    
    async wantsToEat(
        philosopher,
        pickLeftFork,
        pickRightFork,
        eat,
        putLeftFork,
        putRightFork
    ) {
        const left = philosopher;
        const right = (philosopher + 1) % 5;
        
        // Request permission from waiter
        await this.waiter.acquire();
        
        // Pick up forks
        await this.pickFork(left);
        pickLeftFork();
        
        await this.pickFork(right);
        pickRightFork();
        
        // Eat
        eat();
        
        // Put down forks
        this.putFork(left);
        putLeftFork();
        
        this.putFork(right);
        putRightFork();
        
        // Release permission
        this.waiter.release();
    }
    
    async pickFork(forkId) {
        while (this.forks[forkId]) {
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        this.forks[forkId] = true;
    }
    
    putFork(forkId) {
        this.forks[forkId] = false;
    }
}

// Why this works:
// With 5 philosophers and 5 forks, if all 5 try to eat,
// each picks left fork -> deadlock
//
// With waiter limiting to 4 concurrent diners:
// At most 4 can pick forks simultaneously
// 4 philosophers, 5 forks -> at least one can get both forks
// No circular wait possible

4. Reader-Writer Lock Pattern

Variant Priority Fairness Use Case
Readers-Preference Readers get priority Writers may starve Read-heavy workloads
Writers-Preference Writers get priority Readers may starve Write-heavy workloads
Fair (FIFO) First-come-first-served No starvation Balanced workloads
Key Property Multiple readers OR single writer Never both simultaneously Shared data access

Example: Readers-preference lock

class ReadWriteLock {
    constructor() {
        this.readers = 0;
        this.writers = 0;
        this.writeRequests = 0;
    }
    
    async acquireRead() {
        // Wait if any writer is active or waiting
        while (this.writers > 0 || this.writeRequests > 0) {
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        this.readers++;
    }
    
    releaseRead() {
        this.readers--;
    }
    
    async acquireWrite() {
        this.writeRequests++;
        
        // Wait until no readers and no writers
        while (this.readers > 0 || this.writers > 0) {
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        
        this.writeRequests--;
        this.writers++;
    }
    
    releaseWrite() {
        this.writers--;
    }
}

// Usage:
const rwLock = new ReadWriteLock();

// Reader thread:
async function reader() {
    await rwLock.acquireRead();
    try {
        // Read shared data
        console.log('Reading...');
    } finally {
        rwLock.releaseRead();
    }
}

// Writer thread:
async function writer() {
    await rwLock.acquireWrite();
    try {
        // Write shared data
        console.log('Writing...');
    } finally {
        rwLock.releaseWrite();
    }
}

// Multiple readers can run concurrently
// Writer has exclusive access

Example: Fair reader-writer lock

class FairReadWriteLock {
    constructor() {
        this.readers = 0;
        this.writer = false;
        this.queue = []; // FIFO queue
    }
    
    async acquireRead() {
        const ticket = { type: 'read', resolve: null };
        const promise = new Promise(resolve => {
            ticket.resolve = resolve;
        });
        
        this.queue.push(ticket);
        this.processQueue();
        
        await promise;
        this.readers++;
    }
    
    releaseRead() {
        this.readers--;
        this.processQueue();
    }
    
    async acquireWrite() {
        const ticket = { type: 'write', resolve: null };
        const promise = new Promise(resolve => {
            ticket.resolve = resolve;
        });
        
        this.queue.push(ticket);
        this.processQueue();
        
        await promise;
        this.writer = true;
    }
    
    releaseWrite() {
        this.writer = false;
        this.processQueue();
    }
    
    processQueue() {
        if (this.queue.length === 0) return;
        
        const next = this.queue[0];
        
        if (next.type === 'read') {
            if (!this.writer) {
                // Grant all consecutive readers
                while (this.queue.length > 0 && 
                       this.queue[0].type === 'read') {
                    const ticket = this.queue.shift();
                    ticket.resolve();
                }
            }
        } else { // write
            if (!this.writer && this.readers === 0) {
                const ticket = this.queue.shift();
                ticket.resolve();
            }
        }
    }
}

5. Traffic Light Controller System

Component State Synchronization Constraint
Road A/B/C/D Green light allows cars to pass Mutex ensures only one road green Orthogonal roads can't be green together
Light States Green, Yellow, Red State machine transitions Safety: Yellow before Red
Deadlock Prevention Round-robin or priority scheduling Prevent starvation Fair access to all roads
Conflict Detection A ⊥ C, B ⊥ D (perpendicular roads) Mutual exclusion groups Safety critical

Example: Traffic light synchronization

class TrafficLight {
    constructor() {
        // Four roads: 1 (A), 2 (B), 3 (C), 4 (D)
        // Road 1 and 3 are opposite (can be green together)
        // Road 2 and 4 are opposite (can be green together)
        // Road 1 and 2 are perpendicular (cannot be green together)
        
        this.greenRoad = 1; // Currently green road
        this.locks = {
            1: new Semaphore(1),
            2: new Semaphore(0),
            3: new Semaphore(0),
            4: new Semaphore(0)
        };
    }
    
    async carArrived(carId, roadId, direction, turnGreen, crossCar) {
        // Acquire permission for this road
        await this.locks[roadId].acquire();
        
        try {
            // Turn light green
            turnGreen();
            
            // Car crosses intersection
            crossCar();
        } finally {
            // Release lock for this road
            this.locks[roadId].release();
        }
    }
    
    // Alternative: explicit control
    async controlledCarArrived(carId, roadId, direction, turnGreen, crossCar) {
        // Wait for this road to be green
        while (this.greenRoad !== roadId) {
            await new Promise(resolve => setTimeout(resolve, 10));
        }
        
        turnGreen();
        crossCar();
    }
    
    // Traffic controller cycles through roads
    async trafficController() {
        const sequence = [1, 2, 3, 4]; // Round-robin
        
        while (true) {
            for (const roadId of sequence) {
                this.greenRoad = roadId;
                await new Promise(resolve => setTimeout(resolve, 5000)); // 5s green
            }
        }
    }
}

// Simplified version with semaphores
class TrafficLightSimplified {
    constructor() {
        this.roadA = new Semaphore(1); // Initially green
        this.roadB = new Semaphore(0);
    }
    
    async carArrived(carId, roadId, direction, turnGreen, crossCar) {
        if (roadId === 1) {
            await this.roadA.acquire();
            turnGreen();
            crossCar();
            this.roadA.release();
        } else if (roadId === 2) {
            await this.roadB.acquire();
            turnGreen();
            crossCar();
            this.roadB.release();
        }
    }
}

Example: Advanced traffic light with priorities

class AdvancedTrafficLight {
    constructor() {
        this.state = {
            1: 'green',  // North-South
            2: 'red',    // East-West
            3: 'green',  // South-North (opposite of 1)
            4: 'red'     // West-East (opposite of 2)
        };
        
        this.waiting = {
            1: 0, 2: 0, 3: 0, 4: 0
        };
        
        this.mutex = new Semaphore(1);
    }
    
    async carArrived(carId, roadId, direction, turnGreen, crossCar) {
        await this.mutex.acquire();
        
        // Register waiting car
        this.waiting[roadId]++;
        
        // Wait for green light
        while (this.state[roadId] !== 'green') {
            this.mutex.release();
            await new Promise(resolve => setTimeout(resolve, 100));
            await this.mutex.acquire();
        }
        
        this.waiting[roadId]--;
        this.mutex.release();
        
        // Cross intersection
        turnGreen();
        crossCar();
    }
    
    // Scheduler switches lights based on waiting cars
    async scheduler() {
        while (true) {
            await this.mutex.acquire();
            
            // Find road with most waiting cars
            let maxWait = 0;
            let nextRoad = 1;
            
            for (let road = 1; road <= 4; road++) {
                if (this.waiting[road] > maxWait) {
                    maxWait = this.waiting[road];
                    nextRoad = road;
                }
            }
            
            // Switch to that road (and its opposite)
            this.switchLight(nextRoad);
            
            this.mutex.release();
            
            await new Promise(resolve => setTimeout(resolve, 3000));
        }
    }
    
    switchLight(roadId) {
        // Set perpendicular roads to red
        if (roadId === 1 || roadId === 3) {
            this.state[1] = 'green';
            this.state[3] = 'green';
            this.state[2] = 'red';
            this.state[4] = 'red';
        } else {
            this.state[1] = 'red';
            this.state[3] = 'red';
            this.state[2] = 'green';
            this.state[4] = 'green';
        }
    }
}
Warning: Multi-threading requires careful synchronization. Always acquire locks in consistent order to prevent deadlock. Use condition variables instead of busy-waiting to avoid wasting CPU. Remember: mutual exclusion (only one writer), concurrency (multiple readers), and fairness (no starvation).

Summary: Multi-threading Patterns

  • Producer-Consumer: Bounded buffer + condition variables (notFull, notEmpty) - decouples production from consumption
  • Buffer Synchronization: Producer waits when full, consumer waits when empty - signals wake waiting threads
  • Condition Variable Pattern: Lock → check condition → wait atomically releases lock and sleeps → reacquire lock on wake
  • Print in Order: Use semaphores initialized to 0 - release() after completion signals next thread to proceed
  • Semaphore Pattern: Binary semaphore (0 or 1) for mutual exclusion, counting semaphore for resources
  • Dining Philosophers: Resource ordering breaks circular wait - always acquire lower-numbered resource first
  • Deadlock Conditions: Mutual exclusion + Hold and wait + No preemption + Circular wait - break any one to prevent
  • Waiter Solution: Limit concurrent diners to N-1 - guarantees at least one can acquire both forks
  • Reader-Writer Lock: Multiple readers OR single writer - never both simultaneously
  • RW Variants: Readers-preference (readers priority), writers-preference (writers priority), fair (FIFO queue)
  • Starvation Prevention: Fair locks use FIFO queue - process consecutive readers together for efficiency
  • Traffic Light: Mutual exclusion for perpendicular roads - opposite roads can be green together
  • Round-Robin Scheduling: Cycle through states to ensure fairness - prevents starvation of any road
  • Priority Scheduling: Service road with most waiting cars - improves throughput but may cause starvation
  • Lock Ordering: Always acquire locks in same order globally - prevents deadlock from circular dependencies
  • Avoid Busy-Waiting: Use condition variables/semaphores instead of while loops - saves CPU cycles