Techniques to handle deadlocks:
in PProg, design algorithm to not allow deadlocks:

  • via ressource ordering
    • may be “always acquire object with smaller id’s lock first” (see bank account example)
    • or fixed ordering “always hashtable, then map”
  • two phase locking with retry

10.1 Semaphores

Why do we need more than locks?

  • communication: locks don’t provide a way for threads to communicate about changes in state (“data ready”) so we might acquire lock on a queue in a prod/consumer pattern to find the queue empty
  • ordering: locks don’t enforce a specific order for waiting threads
  • ressource counting: either lock is held or not no count of how many

Ownership

A semaphore has no notion of ownership can release even if I didn’t call acquire!

Semantics:

  • A semaphore S is an abstract data type holding a non-negative integer value, initialized to some value .
  • It supports two atomic operations:
    • acquire(S) (or P(S), wait(S))
    • release(S) (or V(S), signal(S))
// Acquire:
// ---- Start Atomic ----
wait until S > 0; // Block if S is zero
S = S - 1;
// ---- End Atomic ----
 
// Release:
// ---- Start Atomic ----
S = S + 1;
// ---- End Atomic ----

Intuition: acquire tries to decrement the count, waiting if it’s zero. release increments the count (potentially waking up a waiting thread).

Semaphore > Lock We can reduce a semaphore to a standard mutex (“binary semaphore”).

  • we initialise with 1 (“unlocked”)
  • lock() decrements to now locked
  • unlock() increments to if another thread was waiting, it can now proceed

Semaphore implementation:

  • usually on OS level or language runtime level
  • blocking queue
  • no spinnning we just update on queue ops

10.1.1 Rendezvouz

Assume computation (ex: dot-product) in two phases, that requires all threads to wait to complete before proceeding (to addition in the dot-product).

If we implement this wrong, it can lead to a deadlock:

To avoid that, we need to separate the signalling and waiting first signal, then wait on the other:

10.2 Barriers

Barrier: A synchronization primitive used to make a number of threads wait until all of them have reached a particular point in the code before any of them are allowed to proceed.

10.2.1 Barrier with turnstile

See shivi https://cs.shivi.io/01-Semesters-(BSc)/Semester-2/Parallel-Programming/Lecture-Notes/18-Deadlocks,-Semaphores,-Barriers

important:

  • count++; not atomic
  • if (count == n) relase(); and all pass through acquire()
    • only one thread actually released others still stuck

if we fix both:

  • turnstile: acquire(); realease(); pattern works, but issue for re-use
    • the last thread called release so on next loop it’s already wide open

10.2.2 Reusable barrier attempt

See shivi https://cs.shivi.io/01-Semesters-(BSc)/Semester-2/Parallel-Programming/Lecture-Notes/18-Deadlocks,-Semaphores,-Barriers

to fix the “last thread left turnstile open” problem, we could add a “reset phase”:

acquire(mutex); count--; release(mutex);
if (count == 0) acquire(barrier);

problem: fast thread might have already run around to complete phase 1 again because we let all threads through phase 2 without “rendezvouzing” again.

10.2.3 Two-Phase Reusable Barrier

Two phases and two barriers:

  • ensure all threads have finished before any can start phase 2
    • only release(barrier1) when are stuck waiting for it
    • in the same move acquire(barrier2) to block that
  • all threads must finish phase 2 before any can start new phase 1
    • release(barrier2) when have passed second phase
    • in the same move, acquire(barrier1) to “relock phase 1” for the next round

the acquire(barrier1); release(barrier1); line is like a turnstile only one thread can go through at a time

  • the acquire blocks all until the -th releases
  • then they all filter through

10.3 Producer-Consumer Pattern

We can use a bounded (circular) FIFO queue to implement this pattern. Producer enqueues, consumer dequeues.

10.3.1 Basic Spinning Implementation

How do we differentiate between full and empty queue?

We can leave an empty space no need for a counter variable that needs to be atomic!

Problem:

// Inside Queue class...
 
public synchronized void enqueue(long item) {
    while (isFull()) {
        ; // Spin-wait (BAD!) - holds the lock!
    }
    doEnqueue(item);
}
 
public synchronized long dequeue() {
    while (isEmpty()) {
        ; // Spin-wait (BAD!) - holds the lock!
    }
    return doDequeue();
}

DEADLOCK! if enqueue finds queue is full, it spins while holding the lock!

10.3.2 Attempt with sleep()

Kind of an unholy child of exponential back-off and busy waiting here.

// Inside Queue class... (Illustrative, enqueue only)
public void enqueue(long item) throws InterruptedException {
    while (true) {
        synchronized(this) { // Acquire lock
            if (!isFull()) {
                doEnqueue(item);
                return; // Success!
            }
            // If full, release lock by exiting synchronized block
        }
        // Lock is released, sleep for a bit before retrying
        Thread.sleep(timeout); // Sleep *without* holding the lock!
    }
}

Problems:

  • no direct notification have to wait until consumer wakes up!!!
  • busy waiting / polling inefficient
  • choosing timeout is hard
    • might have 10s until something happens
    • if too quick livelock maybe

10.3.3 Semaphores

Semaphore have build in counter + waiting mechanism.

Idea: We initialise one with the capacity of the queue it does it all for us.

  • not possible:
    • either we have a semaphore counting free Slots (starts with Semaphore(N))
      • then the producers produce (acquire) until .
      • but there’s no “value = N” (empty) block that would stop consumers from consuming (release)
    • same thing happens if we have it count emptySlots only!
      We always need the “baton-passing” pattern nonFull and nonEmpty with two semaphores and a lock for mutex!

Issues (see https://cs.shivi.io/01-Semesters-(BSc)/Semester-2/Parallel-Programming/Lecture-Notes/19-Producer-Consumer-Pattern,-Queue,-Monitors#producerconsumer-with-semaphores):

  • Deadlock we need to correctly implement to avoid problems!

Acquire the condition semaphore (nonFull or nonEmpty) before acquiring the mutex (manipulation).
thread only gets mutex when it knows it can proceed.

Why Are Semaphores (and Locks) Problematic?

While semaphores work, they are considered somewhat “unstructured”:

  • Discipline Required
  • Deadlock Prone
  • Condition Waiting: Basic locks/semaphores don’t cleanly integrate waiting for an arbitrary condition (like isFull() == false) with mutual exclusion. We either spin (bad) or use complex semaphore arrangements.

What we need: A construct that combines mutual exclusion (like a lock) with the ability to efficiently wait for a specific condition while temporarily releasing the lock, and be notified when that condition might have become true.

10.4 Monitors

Monitors, invented by Tony Hoare and Per Brinch Hansen, provide this higher-level abstraction.

Definition: A monitor is an abstract data structure (like a class or module) where:

  • Data is encapsulated.
  • Operations (methods) on that data execute with implicit mutual exclusion – only one thread can be executing any method of the monitor instance at a time.
  • It includes mechanisms for threads to wait for conditions and signal other threads when conditions change.

How would our queue look conceptually with a monitor?

// Monitor-based Queue (Conceptual)
public monitor class BoundedQueue {
    // internal state (buffer, in, out, etc.)
 
    public void enqueue(long item) { // Implicit mutual exclusion
        while (isFull()) {
            wait_until_not_full(); // Wait if condition not met
        }
        doEnqueue(item);
        signal_that_not_empty(); // Notify waiting consumers
    }
 
    public long dequeue() { // Implicit mutual exclusion
        while (isEmpty()) {
            wait_until_not_empty(); // Wait if condition not met
        }
        long item = doDequeue();
        signal_that_not_full(); // Notify waiting producers
        return item;
    }
}

The monitor handles the locking. We just need primitives to wait for conditions and signal when conditions change.

Monitor Semantics for Condition Handling:

When a thread inside a monitor operation finds a condition doesn’t hold (e.g., isFull() is true):

  • It needs to atomically release the monitor lock.
  • It needs to wait until the condition potentially becomes true.
  • Another thread, upon changing the state (e.g., making the queue not full), needs to signal waiting threads.
  • A waiting thread, upon being signaled, needs to re-acquire the monitor lock before resuming execution.

10.4.2 Monitors in Java (synchronized, wait, notify, notifyAll)

class Queue {
    // ... state variables, constructor, helpers ...
 
    synchronized void enqueue(long x) {
        while (isFull()) { // MUST use while, not if!
            try {
                wait(); // Releases lock on 'this', waits
            } catch (InterruptedException e) { /* handle */ }
            // Re-acquires lock upon waking
        }
        doEnqueue(x);
        notifyAll(); // Notify any waiting consumers
    }
 
    synchronized long dequeue() {
        while (isEmpty()) { // MUST use while, not if!
            try {
                wait(); // Releases lock on 'this', waits
            } catch (InterruptedException e) { /* handle */ }
            // Re-acquires lock upon waking
        }
        long item = doDequeue();
        notifyAll(); // Notify any waiting producers
        return item;
    }
}

Important Questions & Refinements:

  1. while vs if around wait():
    1. You must use while (condition) wait();
      1. Why? Because a thread can wake up from wait() for reasons other than the specific condition being true (spurious wakeups)
      2. or another thread might have changed the condition again between the notify and the woken thread re-acquiring the lock.
    2. The while loop ensures the condition is re-checked after waking. Using if can lead to errors.
  2. notify() vs notifyAll():
    1. Why notifyAll() here?
      1. If we used notify(), we might accidentally wake up another producer when the queue becomes not full (instead of a consumer), or wake a consumer when the queue becomes not empty (instead of a producer).
      2. notifyAll() wakes everyone, and the while loop lets only the threads whose condition is actually met proceed.
    2. notify() can be used as an optimization only if you are certain that any thread woken can make progress and that only threads waiting for the specific signaled condition need to be woken. notifyAll() is generally safer.

10.4.3 Java Monitor Implementation Details

  • wait() moves thread from RUNNABLE to WAITING (or TIMED_WAITING if wait(timeout) is used).
  • notify()/notifyAll() moves waiting threads to BLOCKED
    • compete for re-entry

Monitor Queues each monitor has

  • entry queue with threads waiting to acquire the lock (via synchronized)
  • wait set (condition queue) threads that called wait and are waiting for a notification
    Notification process: when notify/notifyAll is called thread(s) are moved from the wait set entry queue.

In Java: signaling and continue (signaling process continues running signaling process moves signaled process to waiting entry queu)

  • there is also signal and wait hands lock directly to notified
    • not implemented in Java like this

10.4.4 Alternative java.util.concurrent.locks.Condition

Intrinsic locks (synchronized/wait/ notify`) have limitations:

  • One implicit lock per object.
  • wait/notify work on a single, implicit condition set per object.
  • Tied to block structure.

The java.util.concurrent.locks package provides more flexibility:

  • Lock interface (e.g., ReentrantLock) provides explicit lock() and unlock().
  • A Lock object can create multiple Condition objects associated with it using lock.newCondition().

Condition Interface:

  • Each Condition object represents a separate wait set associated with the same Lock.
  • await(): Called while holding the associated Lock.
    • Atomically releases the Lock and waits on this specific condition.
    • Re-acquires the Lock before returning.
    • Must re-check application condition in a while loop. (await stands for atomic wait)
  • signal()/signalAll(): Called while holding the associated Lock. Wakes up one/all thread waiting on this specific condition.

This allows creating more complex synchronization logic, like separate conditions for “not full” and “not empty” associated with the same buffer lock, potentially allowing more targeted signal() calls instead of always using signalAll().

10.4.6 Queue (Producer-Consumer) using Monitors

10.4.7 Sleeping Barber Variant

Solving Sleeping Barber:

  • To avoid lost signals, the barber/consumer needs to know if customers/producers are actually waiting before going to sleep, and customers/producers need to know if the barber/consumer is actually sleeping before trying to wake them.
  • This typically requires additional shared counters to track the number of waiting producers and consumers.

Explanation:

  • The counters n and m are decremented before the wait condition check.
    • A negative value indicates that adding/removing this item might satisfy a waiting thread on the other side.
  • The counters are incremented after the operation that changes the buffer state.
  • Crucially, signal is only called if the corresponding counter was less than or equal to zero before it was incremented (meaning the other type of thread was definitely blocked or about to block).
    • This avoids signaling when no threads are waiting.