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:
- Mutual Exclusion: Resources cannot be shared.
- Hold and Wait: A process holds at least one resource while waiting for others.
- No Preemption: Resources cannot be forcib taken.
- 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.