Skip to content
Back to all concepts
Operating SystemsConcurrencyLow LevelPerformance

Process vs Thread

Understanding the fundamental units of execution in operating systems. A deep dive into memory models, context switching costs, and concurrency implementation details.

By ScaleWikiUpdated 3 min readIntermediate

Core Concepts

When you run a program, the operating system creates an instance of execution. But is it a process or a thread? Understanding the distinction is critical for building high-performance systems.

FeatureProcessThread
DefinitionIndependent program execution unitLightweight unit within a process
MemoryIsolated memory spaceShared memory (heap, code, globals)
CommunicationIPC (pipes, sockets, shm)Shared variables, locking
Context SwitchMay change address-space state; hardware and workload matterSame-process switches retain the address space but still have scheduling/cache costs
FailureIsolated (crash doesn't affect others)Crash kills entire process

Memory Architecture

The key difference lies in how memory is organized.

Process Layout

Each process has its own virtual address space.

Separate virtual address spaces

Each process has its own private memory map. Matching regions in the two processes are separate allocations, not shared memory.

Process A

Private address space
Code segmentProgram instructions
HeapDynamically allocated memory
StackCalls and local execution state
Global dataProcess-wide variables

Process B

Private address space
Code segmentProgram instructions
HeapDynamically allocated memory
StackCalls and local execution state
Global dataProcess-wide variables

OS kernel space

The operating system manages processes and protected kernel memory; application code cannot directly access kernel space.

Thread Layout

Threads live inside a process and share resources.

One address space, shared process resources

Threads belong to one process. They can access its shared code, heap and global data, while keeping separate stacks and register state.

Single process address space

Shared
Code segmentProgram instructions
HeapObjects both threads can access
Global dataProcess-wide variables

Thread 1

Private

Stack

Calls and local execution state

Registers

Current CPU state

Thread 2

Private

Stack

Calls and local execution state

Registers

Current CPU state

Note: Threads share the heap but have their own stacks. A local variable can still reference a shared object, and a pointer to stack data can escape. Separate stacks do not make every operation on local variables thread-safe.

Context Switching Cost

Switching between execution units isn't free. The CPU must save state and load new state.

Process Context Switch (Heavy)

  1. Save registers
  2. Save stack pointer
  3. Switch Page Table Directory (Virtual Memory) 🚨
  4. Update translation state as required; address-space identifiers can avoid a full TLB flush
  5. Load new state

Thread Context Switch (Light)

  1. Save registers
  2. Save stack pointer
  3. Load new state (No memory map change, No TLB flush)

Code Examples

Python: Multiprocessing vs Threading

The usual CPython build uses a Global Interpreter Lock (GIL) for Python bytecode. Optional free-threaded builds and native code that releases the GIL behave differently.

python
import time
import threading
import multiprocessing

def cpu_bound_task():
    count = 0
    while count < 100_000_000:
        count += 1

def run_threading():
    start = time.time()
    t1 = threading.Thread(target=cpu_bound_task)
    t2 = threading.Thread(target=cpu_bound_task)
    t1.start(); t2.start()
    t1.join(); t2.join()
    print(f"Threading: {time.time() - start:.2f}s")
    # Measure on your interpreter; ordinary GIL-enabled CPython serializes this bytecode work.

def run_multiprocessing():
    start = time.time()
    p1 = multiprocessing.Process(target=cpu_bound_task)
    p2 = multiprocessing.Process(target=cpu_bound_task)
    p1.start(); p2.start()
    p1.join(); p2.join()
    print(f"Multiprocessing: {time.time() - start:.2f}s")
    # Processes can execute concurrently on multiple cores; startup and IPC have costs.

if __name__ == "__main__":
    run_threading()
    run_multiprocessing()

Golang: Goroutines (User-Space Threads)

Go uses "Goroutines" which are M:N mapped (M goroutines on N OS threads).

go
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	start := time.Now()

	// Launch 100,000 lightweight threads
	for i := 0; i < 100000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			// Tiny memory footprint (~2KB)
			// Fast context switch in user space
		}()
	}

	wg.Wait()
	fmt.Printf("Spawned 100k goroutines in %v\n", time.Since(start))
}

Real-World Scaling Decisions

Chrome: Multi-Process Architecture

Chromium uses multiple processes with site isolation; tabs, frames, workers, and process reuse do not map to exactly one process per tab.

  • Pros: If one tab crashes (segfault), the browser stays alive. Security isolation (sandboxing).
  • Cons: High memory usage (each tab needs its own memory overhead).

Redis: Single-Threaded Event Loop

Redis uses a single thread for command processing.

  • Why?: Reduces per-command synchronization and per-connection thread overhead; it does not eliminate context switches.
  • Scale: Used I/O multiplexing to handle thousands of connections efficiently.

Apache vs Nginx

  • Apache (Prefork): A pool of worker processes, with a process handling one connection at a time. Stable but memory hungry.
  • Nginx (Event-Driven): One process handles thousands of requests via async I/O.

Selection Guide

Choose Processes When:

  • You need high isolation (security/stability).
  • The task is CPU-bound (in Python/Ruby with GIL).
  • Failure in one unit shouldn't crash the system.

Choose Threads When:

  • You need to share a lot of data.
  • High frequency communication is required.
  • Creating thousands of units (though async/coroutines are better here).
  • The task is I/O bound.

Interview Tips 💡

  • "Is it better to use threads or processes?" — Always answer "It depends". Mention isolation vs sharing.
  • "What is the GIL?" — Explain the limits of bytecode execution in a GIL-enabled CPython build and the exceptions for native code and free-threaded builds.
  • "User vs Kernel Threads" — Kernel threads are managed by OS (expensive). User threads (Green threads/Goroutines) are managed by runtime (cheap).
  • "Copy-on-Write (COW)" — Explain how fork() is optimized by sharing memory pages until a write occurs.

Source and practical check

Python threading documentation covers the GIL and free-threaded builds. Compare the same workload with threads and processes, including startup, serialization, shared-state locking, and memory use. Neither model universally wins; an I/O-bound test and a Python-bytecode CPU test answer different questions.