OPERATING SYSTEMS / 4. PROCESSES & THREADS

Processes, Threads & Concurrency

How the OS runs many programs at once on a few cores


EXPLANATION

The illusion your OS creates is that every program has the entire CPU to itself. In reality, the OS switches between programs thousands of times per second so fast it feels simultaneous.

Process vs Thread:
• A process is an isolated running program with its own virtual address space, file descriptors, and at least one thread
• A thread is a unit of execution within a process. Multiple threads share the process's address space, file descriptors, and heap — but each has its own stack and registers
• Creating a process (fork) is expensive — copies the address space, file descriptor table
• Creating a thread is cheap — just allocates a new stack and kernel thread structure

Why threads? Parallelism and responsiveness:
• A web server with one thread can only handle one request at a time. With 100 threads, it handles 100 simultaneously
• A UI app needs one thread for the UI (must respond to clicks) and another for background work (download, process)

Thread safety and races:
• If two threads read/write a shared variable without coordination → race condition → undefined behavior
• Solution: mutual exclusion. Mutex (lock) ensures only one thread executes a critical section at a time
• Lock → do work → unlock
• Deadlock: Thread A holds Lock 1, waits for Lock 2. Thread B holds Lock 2, waits for Lock 1. Both wait forever.

Python's GIL (Global Interpreter Lock):
• CPython has a GIL — only one thread executes Python bytecode at a time
• Threading in Python gives concurrency (great for I/O-bound work: HTTP requests, DB queries)
• But NOT true parallelism for CPU-bound work
• For CPU parallelism in Python: use multiprocessing (separate processes, no GIL) or async/await for I/O

The Scheduler — how the OS decides who runs:
• Linux uses CFS (Completely Fair Scheduler)
• Each process has a priority (nice value, -20 to +19). Lower = higher priority
• CFS tracks "virtual runtime" — how much CPU each process has used
• Always runs the process with the least virtual runtime
• Preemption: timer interrupt fires → scheduler checks → may switch to another process

Context switch cost: ~1-10 µs. Involves saving all CPU registers, TLB flush, cache effects. Too many context switches = performance degradation.

Process isolation mechanisms:
• Virtual memory → different page tables → cannot access each other's memory
• Capabilities → fine-grained permissions (open network sockets, access /dev, etc)
• namespaces → what the process can see (network, PID, mount, user — this is how Docker works)
• cgroups → limits on resources (CPU%, RAM, I/O) — also how Docker enforces container limits

DIAGRAM

PROCESS vs THREAD:
  ┌─────────────────────────────────────────────┐
  │              Process                        │
  │  Virtual Address Space                      │
  │  ┌──────────┐ ┌──────────┐ ┌──────────┐   │
  │  │ Thread 1 │ │ Thread 2 │ │ Thread 3 │   │
  │  │  Stack   │ │  Stack   │ │  Stack   │   │
  │  │ Registers│ │ Registers│ │ Registers│   │
  │  └──────────┘ └──────────┘ └──────────┘   │
  │       │            │            │           │
  │       └────────────┴────────────┘           │
  │                    ↓                        │
  │        Shared: Heap, Code, Files            │
  └─────────────────────────────────────────────┘

  SCHEDULER (CFS — Completely Fair Scheduler):
  Timeline:
  Core 0: [Process A]─[Process B]─[Process A]─[Process C]─...
  Core 1: [Process D]─[Process E]─[Process D]─[Process E]─...

  Each slice: ~1-4ms
  Switch triggered by: timer interrupt, blocking syscall, yield

  CONTEXT SWITCH:
  Running A → Interrupted → Save A's registers to PCB →
  Load B's registers from PCB → Run B

CODE

PYTHON
1import threading
2import multiprocessing
3import time
4import random
5from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
6
7# ── Threading: I/O-bound (good use case) ──────────────
8def simulate_network_call(task_id: int) -> str:
9 time.sleep(random.uniform(0.1, 0.3)) # simulate I/O wait
10 return f"Task {task_id} done"
11
12print("=== Threading (I/O bound) ===")
13start = time.perf_counter()
14with ThreadPoolExecutor(max_workers=10) as executor:
15 futures = [executor.submit(simulate_network_call, i) for i in range(20)]
16 results = [f.result() for f in futures]
17print(f"20 I/O tasks with threads: {time.perf_counter()-start:.2f}s")
18
19# Sequential for comparison
20start = time.perf_counter()
21results = [simulate_network_call(i) for i in range(20)]
22print(f"20 I/O tasks sequential: {time.perf_counter()-start:.2f}s")
23
24# ── Multiprocessing: CPU-bound (good use case) ────────
25def cpu_intensive(n: int) -> int:
26 """Compute sum of squares pure CPU work"""
27 return sum(i*i for i in range(n))
28
29print("
30=== Multiprocessing (CPU bound) ===")
31tasks = [10_000_000] * 4
32
33start = time.perf_counter()
34with ProcessPoolExecutor(max_workers=4) as executor:
35 results = list(executor.map(cpu_intensive, tasks))
36print(f"4 CPU tasks (4 processes): {time.perf_counter()-start:.2f}s")
37
38start = time.perf_counter()
39results = [cpu_intensive(n) for n in tasks]
40print(f"4 CPU tasks (sequential): {time.perf_counter()-start:.2f}s")
41
42# ── Race condition demo ────────────────────────────────
43print("
44=== Race Condition vs Mutex ===")
45counter = 0
46lock = threading.Lock()
47
48def increment_unsafe():
49 global counter
50 for _ in range(100_000):
51 counter += 1 # NOT thread safe — read-modify-write
52
53def increment_safe():
54 global counter
55 for _ in range(100_000):
56 with lock: # mutex prevents concurrent access
57 counter += 1
58
59# Unsafe (run without lock)
60counter = 0
61threads = [threading.Thread(target=increment_unsafe) for _ in range(5)]
62for t in threads: t.start()
63for t in threads: t.join()
64print(f"Unsafe counter (should be 500000): {counter}")
65
66# Safe (with lock)
67counter = 0
68threads = [threading.Thread(target=increment_safe) for _ in range(5)]
69for t in threads: t.start()
70for t in threads: t.join()
71print(f"Safe counter (should be 500000): {counter}")
← PREV3. The KernelNEXT →5. Memory Management