Process Management in Operating Systems: Synchronization, Semaphores, and Deadlock

Process management is a foundational component of any operating system, responsible for creating, scheduling, suspending, and terminating processes. A process represents an executing instance of a program, encapsulating its code, runtime data, and state. Efficient process management ensures system stability, resource fairness, and optimal performance under concurrent workloads.

Synchronization and Mutual Exclusion

When multiple processes or threads access shared resources—such as memory locations, files, or hardware devices—without coordination, race conditions can occur. These lead to unpredictable behavior, data corruption, or inconsistent system states. To prevent this, synchronization mechanisms enforce mutual exclusion: only one thread or process may access a critical section at a time.

Mutual Exclusion with Mutexes

A mutex (mutual exclusion lock) is a binary semaphore used to protect shared resources. Only the thread that acquires the mutex may enter the critical section; others must wait untill it is released.

import threading

bank_balance = 100
lock = threading.Lock()

def deposit(amount):
    global bank_balance
    with lock:
        current = bank_balance
        bank_balance = current + amount
        print(f"Deposited {amount}, new balance: {bank_balance}")

def withdraw(amount):
    global bank_balance
    with lock:
        if bank_balance >= amount:
            current = bank_balance
            bank_balance = current - amount
            print(f"Withdrew {amount}, new balance: {bank_balance}")
        else:
            print("Insufficient funds")

# Simulate concurrent access
t1 = threading.Thread(target=deposit, args=(50,))
t2 = threading.Thread(target=withdraw, args=(70,))
t1.start(); t2.start()
t1.join(); t2.join()

In this example, the lock ensures that deposits and withdrawals are serialized, eliminating the risk of incorrect final balances due to interleaved operations.

Critical Resources

A critical resource is any entity that must be accessed exclusively by one thread at a time. Common examples include:

  • Global variables modified by multiple threads
  • Shared log files written by concurrent processes
  • Hardware peripherals like printers or network interfaces

Without proper synchronization, concurrent access can result in interleaved output, corrupted files, or inconsistent state. For instance, two threads writing to the same log file without locking may produce garbled entries:

import threading

log_lock = threading.Lock()

def write_log(message):
    with log_lock:
        with open("app.log", "a") as f:
            f.write(f"[{threading.current_thread().name}] {message}\n")

threads = [
    threading.Thread(target=write_log, args=("Error: DB timeout",)),
    threading.Thread(target=write_log, args=("Info: User logged in",))
]

for t in threads:
    t.start()
for t in threads:
    t.join()

Here, the lock ensures atomic, non-overlapping writes, preserving log integrity.

Binary Semaphores as Mutexes

A semaphore is a more general synchronization primitive that maintains an integer counter. A binary semaphore (initialized to 1) behaves identically to a mutex:

  • P() (wait): Decrements the counter. If the value becomes negative, the thread blocks.
  • V() (signal): Increments the counter. If there are waiting threads, one is unblocked.
import threading
import time

printer = threading.Semaphore(1)  # Binary semaphore

def print_document(doc_name):
    printer.acquire()
    print(f"Printing: {doc_name}")
    time.sleep(2)
    print(f"Finished printing {doc_name}")
    printer.release()

t1 = threading.Thread(target=print_document, args=("Invoice.pdf",))
t2 = threading.Thread(target=print_document, args=("Report.docx",))

t1.start(); t2.start()
t1.join(); t2.join()

Both mutexes and binary semaphores ensure exclusive access, but semaphores offer greater flexibility for managing multiple identical resources.

Productionn-Consumption Problem with Counting Semaphores

The classic producer-consumer problem involves coordinating threads that generate and consume data from a bounded buffer. Two counting semaphores are used:

  • empty: Tracks available buffer slots (initially N)
  • full: Tracks filled slots (initially 0)

A third mutex is often used to protect buffer access, though some implementations omit it if atomic operations are guaranteed.

from threading import Thread, Semaphore
import time

BUFFER_SIZE = 5
empty = Semaphore(BUFFER_SIZE)
full = Semaphore(0)
buffer = []

def producer():
    for i in range(10):
        empty.acquire()        # Wait for empty slot
        buffer.append(i)
        print(f"Produced: {i}")
        full.release()         # Signal available item
        time.sleep(0.5)

def consumer():
    for _ in range(10):
        full.acquire()         # Wait for item
        item = buffer.pop(0)
        print(f"Consumed: {item}")
        empty.release()        # Signal free slot
        time.sleep(1)

p = Thread(target=producer)
c = Thread(target=consumer)

p.start(); c.start()
p.join(); c.join()

This design prevents buffer overflow (producer waits if full) and underflow (consumer waits if empty), enabling safe concurrent operation.

Deadlock: Causes and Prevention

Deadlock occurs when two or more processes are blocked indefinitely, each waiting for a resource held by another. Four necessary conditions must coexist:

  1. Mutual Exclusion: Resources cannot be shared.
  2. Hold and Wait: A process holds at least one resource while waiting for others.
  3. No Preemption: Resources cannot be forcib taken.
  4. Circular Wait: A cycle exists in the resource allocation graph.

Consider two threads and two locks:

import threading
import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_a():
    with lock1:
        time.sleep(1)
        with lock2:
            print("Thread A acquired both locks")

def thread_b():
    with lock2:
        time.sleep(1)
        with lock1:
            print("Thread B acquired both locks")

a = threading.Thread(target=thread_a)
b = threading.Thread(target=thread_b)

a.start(); b.start()
a.join(); b.join()  # Potential deadlock

If both threads acquire their first lock simultaneously, each waits for the other’s lock—deadlock occurs.

Deadlock Avoidance and Prevention

Prevention strategies break one of the four conditions:

  • Require all resources to be requested upfront (eliminates hold-and-wait).
  • Enforce a global ordering on resource acquisition (breaks circular wait).

Avoidance uses algorithms like the Banker’s Algorithm to dynamically check if a resource allocation leads to a safe state.

Detection and Recovery periodically checks for cycles in resource graphs. If detected, recovery may involve terminating one or more processes.

The minimum number of resources required to prevent deadlock among n processes, each needing r units, is:

n × (r - 1) + 1

Threads vs. Processes

While a process is an independent execution unit with its own memory space and system resources, a thread is a lightweight entity within a process.

Aspect Process Thread
Resource Ownership Owns memory, files, devices Shares process resources
Creation Overhead High (new address space) Low (shared context)
Context Switch Expensive Fast
Communication Requires IPC (pipes, sockets) Direct via shared memory
Isolation High (crash doesn't affect others) Low (crash can kill entire process)

Modern systems leverage threads to exploit multi-core processors efficiently. However, shared memory introduces complexity: proper synchronization is essential to avoid data races and ensure correctness.

Tags: process-synchronization mutex Semaphore deadlock producer-consumer

Posted on Sat, 26 Sep 2026 16:39:45 +0000 by kwdelre