Skip to content

Concurrency Control (Locks, Deadlocks, Semaphores)

Modern operating systems and databases must handle concurrent execution of multiple processes/threads.
Without proper synchronization, concurrency can cause:

  • Race conditions (wrong/inconsistent results).
  • Deadlocks (no progress).
  • Starvation (some processes never get a chance).

Concurrency control techniques ensure correctness, fairness, and efficiency when processes share resources (files, memory, CPU, devices).

1. Locks

A lock enforces mutual exclusion (mutex): only one process can access a critical section at a time.

Types of Locks

  1. Mutex (Mutual Exclusion Lock)

    • Binary state (locked/unlocked).
    • Only the owner can release it.
  2. Spinlock

    • Busy-waiting: thread continuously checks if lock is free.
    • Good for short critical sections on multiprocessors (avoids context-switch overhead).
  3. Read-Write Lock (RWLock)

    • Multiple readers can hold the lock simultaneously.
    • Writers get exclusive access.
    • Example: Database queries (many readers, few writers).
  4. Ticket/Fair Locks

    • Processes get a "ticket number," ensuring FIFO order to prevent starvation.

Locking Problems

  • Priority Inversion: A high-priority task waits because a low-priority task holds a lock.
    • Solution: Priority inheritance protocol (boost priority temporarily).
  • Deadlock: If two locks are acquired in different orders, deadlock may occur.
  • Starvation: If scheduling favors some processes, others may never acquire lock.

Example (Python-like pseudo-code):

python
lock.acquire()
# critical section
lock.release()

2. Semaphores

A semaphore is a synchronization primitive represented as a protected integer variable.
It regulates how many processes/threads can access a resource at once, using two atomic operations: wait() and signal().

2.1 The Role of the Integer Value

  • The integer value represents the number of available units of a resource.

  • When a process calls wait():

    • The value is decremented.
    • If the value is still ≥ 0, the process can proceed (resource granted).
    • If the value becomes < 0, the process is blocked and placed in a wait queue.
  • When a process calls signal():

    • The value is incremented.
    • If the value is ≤ 0, it means there are blocked processes → one of them is woken up.

In short:

  • Positive / Zero value → free resources available.
  • Negative value → absolute value = number of processes waiting in the queue.

2.2 Types of Semaphores

  1. Binary Semaphore

    • Integer is either 0 or 1.
    • Only one process can hold it at a time (mutex behavior).
  2. Counting Semaphore

    • Integer starts at N (number of identical resources).
    • Multiple processes can acquire the semaphore simultaneously, as long as the counter stays ≥ 0.
    • Example: If 3 printers are available, semaphore initialized to 3 → up to 3 processes can print at once.

2.3 Example: Counting Semaphore (3 Printers)

python
sem = Semaphore(3)   # 3 printers available

def print_job(job):
    sem.wait()       # tries to take 1 printer
    use_printer(job) # critical section
    sem.signal()     # releases printer

How it works:

  • At start: sem = 3.
  • First process calls wait(), sem → 2 (allowed).
  • Second process calls wait(), sem → 1 (allowed).
  • Third process calls wait(), sem → 0 (allowed).
  • Fourth process calls wait(), sem → -1 → blocked until one of the others calls signal().

This is how multiple processes can hold the semaphore "lock" at the same time:
because the counter tracks how many resources are left, not just a binary lock.

2.4 Example: Binary Semaphore (Mutex)

python
mutex = Semaphore(1)  # only 1 at a time

def update_balance(amount):
    mutex.wait()        # lock
    balance += amount   # critical section
    mutex.signal()      # unlock
  • If one process holds it, the counter = 0 → next wait() will block.
  • Acts just like a mutex.

2.5 Real-World Uses

  • Binary Semaphore → Mutual exclusion (critical section).
  • Counting Semaphore → Resource pool management (threads, printers, DB connections).
  • Producer-Consumer → Classic problem solved with two semaphores (empty, full).

2.6 Summary of Integer Meaning

Semaphore ValueMeaning
> 0Number of resources immediately available.
= 0No free resource, but no one waiting yet.
< 0Number of processes waiting (absolute value).

2.7 Analogy

  • Binary Semaphore (1) = one bathroom key → only one person can enter.
  • Counting Semaphore (N) = parking lot with N slots → N cars can park at once.
    • If lot full → arriving cars wait (value goes negative).

3. Race Conditions

A race condition occurs when the final outcome depends on the timing of concurrent execution.

Example: Bank Account

  • Initial balance = 100.
  • Thread A deposits +50, Thread B withdraws -30 at the same time.
  • Without lock:
    • Both read balance = 100.
    • A writes 150, B writes 70 → final = 70 (deposit lost).

Solution: Protect shared data with mutexes, semaphores, or atomic operations.

4. Deadlocks

A deadlock happens when processes are stuck waiting forever for resources held by each other.

Coffman’s 4 Conditions (necessary for deadlock)

  1. Mutual Exclusion → resources not sharable.
  2. Hold and Wait → process holds one resource and waits for another.
  3. No Preemption → resources cannot be forcibly taken away.
  4. Circular Wait → cycle of processes each waiting for resource held by another.

If all four hold → deadlock possible.

Deadlock Handling

  1. Prevention → Break one condition.
    • No circular wait (impose order on resource acquisition).
    • Preempt resources when necessary.
  2. Avoidance → Use algorithms to avoid unsafe states.
    • Banker’s Algorithm: ensures system always remains in a “safe” state.
  3. Detection & Recovery
    • Construct wait-for graph → detect cycles.
    • Kill/restart process or preempt resources.

5. Starvation & Livelock

  • Starvation: Process waits indefinitely (e.g., low-priority task always skipped).
  • Livelock: Processes keep changing state but make no progress (e.g., two people stepping aside repeatedly in a hallway).

Solutions:

  • Use fair locks (FIFO order).
  • Aging → gradually increase priority of waiting processes.

6. Monitors & Condition Variables

A monitor is a high-level synchronization construct that combines:

  1. Mutual exclusion (only one thread active inside at a time).
  2. Condition variables (to allow threads to wait and be signaled when certain conditions are met).

Monitors simplify concurrent programming because the compiler/runtime enforces mutual exclusion automatically.
This makes them safer and less error-prone than raw semaphores.

6.1 Why Monitors?

  • With semaphores, programmers must manually wait() and signal() in the right places → error-prone.
  • Monitors encapsulate synchronization with data → making code easier to reason about.
  • Languages like Java, C#, Go, Python (threading.Condition) provide built-in monitor-like constructs.

6.2 Condition Variables Inside Monitors

A condition variable (CV) is used to wait for a condition to become true.
It provides two key operations:

  • wait(cv) → releases the monitor lock and suspends the calling thread until another thread signals cv.
  • signal(cv) → wakes up one waiting thread (if any).

Important difference from semaphores:

  • Condition variables do not have counters.
  • They are just waiting lists associated with conditions inside a monitor.

6.3 Example: Producer-Consumer with Monitor

python
monitor Buffer {
    condition notEmpty, notFull
    queue data
    int capacity = 5

    procedure insert(item):
        if data.size == capacity:
            wait(notFull)         # release lock, go to sleep
        data.enqueue(item)
        signal(notEmpty)          # wake up one waiting consumer

    procedure remove():
        if data.empty():
            wait(notEmpty)        # release lock, go to sleep
        item = data.dequeue()
        signal(notFull)           # wake up one waiting producer
        return item
}

How this works:

  • Mutual exclusion: Only one process runs inside the monitor at any given time.
  • Condition variables:
    • If buffer is full → producers wait on notFull.
    • If buffer is empty → consumers wait on notEmpty.
  • Signal: Wakes up a thread waiting on that condition.

6.4 Real-World Implementations

  • Java:

    • Every object has an implicit monitor.
    • synchronized methods/blocks enforce mutual exclusion.
    • wait() and notify()/notifyAll() act like condition variable operations.
    java
    class Buffer {
        private Queue<Integer> data = new LinkedList<>();
        private int capacity = 5;
    
        public synchronized void insert(int item) throws InterruptedException {
            while (data.size() == capacity) wait();
            data.add(item);
            notify(); // signal notEmpty
        }
    
        public synchronized int remove() throws InterruptedException {
            while (data.isEmpty()) wait();
            int item = data.remove();
            notify(); // signal notFull
            return item;
        }
    }
  • pthreads (C):

    • Uses pthread_mutex_t (lock) + pthread_cond_t (condition variables).
  • Python (threading.Condition):

    • Encapsulates a lock + condition variable.

6.5 Monitors vs Semaphores

AspectSemaphoreMonitor
LevelLow-level primitiveHigh-level abstraction
EncapsulationSeparate from dataBundled with shared data
ResponsibilityProgrammer must handle correctnessCompiler/runtime enforces correctness
Condition handlingCounters (wait decreases, signal increases)Explicit wait/signal on condition vars
Error-prone?High (forgotten signals, deadlocks)Lower (built-in discipline)

6.6 Analogy

  • Semaphore = doorman with a counter: lets N people in, others wait.
  • Monitor = automatic door + waiting lounge:
    • Only one person inside room.
    • If room isn’t ready, people wait in lounge (condition variables).
    • Once ready, system automatically lets someone in.

Languages like Java (synchronized blocks) and pthreads implement monitors.

7. Concurrency in Real Systems

  • Databases: Use 2-phase locking, transactions, isolation levels.
  • Linux Kernel: Spinlocks, semaphores, RCU (Read-Copy-Update).
  • Java: synchronized, ReentrantLock, Semaphore, CountDownLatch.

8. Interview Tips

  • Mutex vs Semaphore: Mutex has single owner, semaphore is counter.
  • Deadlock conditions: Be able to state all four.
  • Banker’s Algorithm: Explain “safe state” with small example.
  • Starvation vs Deadlock: Deadlock = stuck, Starvation = ignored.
  • Real-world examples:
    • Locks: File system access.
    • Semaphores: Controlling DB connections.
    • Deadlock: Two trains on single track, both waiting.

Analogies:

  • Lock = one bathroom key.
  • Semaphore = parking lot with limited slots.
  • Deadlock = two cars stuck on a one-lane bridge.
  • Starvation = a car always letting others go first, never moving.
  • Livelock = two people stepping aside endlessly, blocking each other.

9. (Bonus) Banker’s Algorithm (Deadlock Avoidance)

The Banker’s Algorithm (by Dijkstra) is used for deadlock avoidance.
It ensures the system never enters an unsafe state where deadlock is possible.

Key Ideas

  • Each process must declare the maximum resources it may need.
  • The OS only grants resource requests if, after allocation, the system can still remain in a safe state (i.e., there exists a sequence in which all processes can finish).

Data Structures

  • Available[] → number of available instances of each resource type.
  • Max[][] → maximum demand of each process.
  • Allocation[][] → resources currently allocated to each process.
  • Need[][] = Max - Allocation → remaining resources each process still needs.

Safety Algorithm (to check if system is safe)

  1. Initialize Work = Available, Finish[i] = false.
  2. Find a process i such that:
    • Finish[i] == false, and
    • Need[i] <= Work.
  3. If found → pretend to allocate, set Work = Work + Allocation[i], Finish[i] = true.
  4. Repeat until no such process is found.
  5. If all Finish[i] == true, the system is in a safe state.

Example

Suppose we have 3 processes (P0, P1, P2) and 3 resource types (A, B, C).

  • Total instances: A=10, B=5, C=7.
  • Current allocation and maximum demand:
ProcessAllocation (A B C)Max (A B C)
P00 1 07 5 3
P12 0 03 2 2
P23 0 29 0 2

Step 1: Compute Need = Max - Allocation

ProcessNeed (A B C)
P07 4 3
P11 2 2
P26 0 0

Step 2: Compute Available Available = Total - Sum(Allocation) = (10, 5, 7) - (0+2+3, 1+0+0, 0+0+2) = (5, 4, 5)

Step 3: Apply Safety Algorithm

  • Work = (5,4,5).
  • P1’s Need (1,2,2) ≤ Work → allocate to P1 → Work = Work + Allocation(P1) = (7,4,5).
  • P2’s Need (6,0,0) ≤ Work → allocate → Work = (10,4,7).
  • P0’s Need (7,4,3) ≤ Work → allocate → Work = (10,5,7).

All processes finish → Safe sequence = P1 → P2 → P0.

✅ The system is in a safe state, so the request is granted.


Interview Tip

If asked about Banker’s Algorithm:

  • State clearly that it’s for deadlock avoidance (not prevention/detection).
  • Mention: works like a bank checking if a loan can be granted without risk.
  • Always mention safe state and the idea of “pretend allocation, then check.”

Analogy:
Bank doesn’t give out loans unless it knows it can still satisfy everyone eventually → hence the name Banker’s Algorithm.


Connect: LinkedIn

© 2025 Official CTO. All rights reserved.