Processes, threads, process states and context switching
40 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)In almost every campus and off-campus technical round, the first operating-system question is "process vs thread?". The interviewer is checking that you can say what is shared and what is private, not just recite definitions.
What an OS does: kernel, user mode and system calls
Kernel is the core of the operating system that stays in memory, runs in kernel mode and controls the CPU, memory, disks and devices.
The program below asks the operating system three questions through Python: its name, how many CPU cores it offers, and the id of this running program.
Example 1 · Asking the OS through Python
import osimport platformimport threading
print("OS name:", platform.system())print("CPU cores the OS gives Python:", os.cpu_count())print("This process id:", os.getpid())print("Threads in this process right now:", threading.active_count())OS name: Windows CPU cores the OS gives Python: 12 This process id: 7524 Threads in this process right now: 1
What this code does
- Each line is answered by the OS: Python asks the kernel (through system calls made by the interpreter) for the core count and the process id.
- Your values will differ: the OS name, core count and process id depend on your computer and on this run.
The OS is a resource manager: it decides which program runs, how much memory each gets and who may open which file. In kernel mode the CPU lets code do anything, such as talk to the disk; in user mode, where your programs run, such actions are blocked.
A program gets a blocked action done through a system call: a controlled request to the kernel, such as read, write, fork or exit. Python hides these behind its library; Path.read_text() ends up in open and read system calls.
Find the bug
Spot the wrong line
Each spoken answer has one wrong line. Click it.
Answer to "What is a system call?"
Round 1 of 2Process vs thread: memory, cost, communication
Thread is a path of execution inside a process; threads of one process share its code, heap and open files, but each thread has its own stack, program counter and registers.
Two threads of one process write into the same array, and the main thread reads both results straight away, with no pipes or sockets in between.
Example 2 · Two threads share one process's memory
import threading
votes = [0, 0] # one list in shared memory: every thread sees it
def count_votes(slot, n): for _ in range(n): votes[slot] += 1
hostel_a = threading.Thread(target=count_votes, args=(0, 300))hostel_b = threading.Thread(target=count_votes, args=(1, 450))hostel_a.start()hostel_b.start()hostel_a.join() # wait for both threads to finishhostel_b.join()
print("Hostel A votes:", votes[0])print("Hostel B votes:", votes[1])print("Main thread sees both:", sum(votes))Hostel A votes: 300 Hostel B votes: 450 Main thread sees both: 750
What this code does
- Both threads write into the same
voteslist, and the main thread reads the results directly. No pipes or sockets are needed because threads share memory. - Each thread updates its own slot, so there is no conflict. When two threads update the same value, a race condition can appear even with the GIL, because
+=is several steps; the Synchronisation and semaphores lesson later in this module covers that.
A process is a program in execution, with its own address space (code, data, heap and stack), open files and an id. The four differences interviewers ask for:
- Memory: a process has a separate address space; threads share the heap and have their own stacks.
- Cost: creating or switching a thread is usually cheaper than a process.
- Communication: threads use shared objects; processes need inter-process communication (IPC) such as pipes, sockets or shared memory.
- Failure: a crashed process is isolated; an uncaught error can stop a whole multi-threaded process.
In Python, a threading.Thread is a real OS thread, but the GIL (global interpreter lock) of CPython lets only one thread run Python bytecode at a time. Threads help while a program waits for files or the network; for CPU-heavy work use multiprocessing, which starts separate processes with their own memory.
Predict the output
What do the threads see?
Pick the output. Remember what threads share.
Round 1 of 2
A thread changes a module-level variable and main waits for it.
Process states and the PCB
Process state is the stage of a process's life: new, ready, running, waiting or terminated, and the scheduler and I/O events move it between them.
The program below walks one process through its states and records each move, with a small Pcb class holding what the kernel needs to resume it.
Example 3 · A process moving through its states
from dataclasses import dataclass, fieldfrom enum import Enum
class State(Enum): NEW = 1 READY = 2 RUNNING = 3 WAITING = 4 TERMINATED = 5
@dataclassclass Pcb: pid: int state: State = State.NEW program_counter: int = 0 history: list = field(default_factory=lambda: ["NEW"])
def move_to(self, nxt): self.state = nxt self.history.append(nxt.name)
p = Pcb(101)p.move_to(State.READY) # admitted by the OSp.move_to(State.RUNNING) # picked by the schedulerp.program_counter = 40p.move_to(State.WAITING) # asks to read a filep.move_to(State.READY)p.move_to(State.RUNNING)p.move_to(State.TERMINATED)
print(f"PID {p.pid}: " + " -> ".join(p.history))print("Resumed at program counter", p.program_counter)PID 101: NEW -> READY -> RUNNING -> WAITING -> READY -> RUNNING -> TERMINATED Resumed at program counter 40
What this code does
Pcbkeeps the fields the kernel needs to pause and resume a process: id, state and program counter (a real PCB has much more).- After waiting for I/O the process goes back to READY, not straight to RUNNING: it must wait for the scheduler to pick it again.
A process starts new, is admitted to ready (waiting for the CPU), becomes running when the scheduler picks it, moves to waiting when it needs I/O, and ends terminated. Only one process per CPU core is running at any moment.
The PCB (process control block) holds the process id, state, program counter, saved registers, priority, the page table, the list of open files and the CPU time used.
Order of execution
A process reads a file
Put the states of one process in order, from creation to exit.
Context switching and its cost
Context switch is the kernel stopping one process or thread and starting another: it saves the running one's state in its PCB and loads the next one's state.
The program below shows how much CPU time context switching wastes for four time-slice lengths, assuming each switch costs 5 microseconds.
Example 4 · How much CPU time switching wastes
def useful_percent(slice_us, switch_us): return 100.0 * slice_us / (slice_us + switch_us)
switch_cost = 5 # microseconds per context switch (assumed)for slice_us in (10, 100, 1000, 10000): print(f"Slice {slice_us:6.0f} us -> {useful_percent(slice_us, switch_cost):.1f}% useful work")Slice 10 us -> 66.7% useful work Slice 100 us -> 95.2% useful work Slice 1000 us -> 99.5% useful work Slice 10000 us -> 100.0% useful work
What this code does
- Every slice of work is followed by one switch. With a 10 µs slice, a third of the CPU goes to switching; with 10 ms it is negligible.
- This is the trade-off schedulers face: switch often for quick response, or rarely for less overhead. The 5 µs figure is an assumption for the sum.
During a switch no user work is done, so it is pure overhead. Switching between processes also changes the memory map, which flushes or weakens the caches and the TLB (a cache of address translations). Threads of one process skip that change, which is why their switch is cheaper.
Exact costs vary by hardware and OS: often a few microseconds, plus the cache effects afterwards.
Predict the output
How much work is useful?
A switch costs 5 microseconds. Pick the useful percentage.
Round 1 of 2
Time slice 20 microseconds.
Interview questions with model answers
The program below shows the life cycle of a Python thread, which maps closely onto the operating system states.
Example 5 · Seeing a thread's life cycle in Python
import threading
def work(): total = sum(range(1, 101))
worker = threading.Thread(target=work)print(f"Before start: alive={worker.is_alive()}, started={worker.ident is not None}")worker.start()worker.join()print(f"After join: alive={worker.is_alive()}, started={worker.ident is not None}")me = threading.current_thread()print(f"Main thread: alive={me.is_alive()}, started={me.ident is not None}")Before start: alive=False, started=False After join: alive=False, started=True Main thread: alive=True, started=True
What this code does
- Python has no
NEWorTERMINATEDnames.is_alive()is False beforestart()and again after the thread finishes, soident(None until the thread has started) tells the two apart. - The main thread is alive while it prints. While a thread is alive, the OS moves it between ready, running and waiting without Python showing which.
Python does not name the states. is_alive() is true from start() until run() ends. While a thread is alive, the OS moves it between ready, running and waiting; a thread stuck in lock.acquire() or time.sleep() is in the waiting state. Interviewers like this link because it connects OS theory to code you write.
Next step
Check your understanding
12 quick questions