Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Threads vs Processes

Understanding the fundamental building blocks of concurrent execution.

Before diving into concurrency patterns, it’s crucial to understand the fundamental building blocks: processes and threads. These concepts form the foundation of all concurrent programming.

Diagram

A process is an independent program running in its own memory space. Each process has:

  • Isolated memory - Cannot directly access another process’s memory
  • Own code and data - Separate copy of program code and data
  • Own resources - File handles, network connections, etc.
  • Process ID (PID) - Unique identifier assigned by the operating system
Diagram

A thread is a lightweight unit of execution within a process. Multiple threads share:

  • Same memory space - All threads in a process share code, data, and heap
  • Same resources - File handles, network connections, etc.
  • Separate stacks - Each thread has its own stack for local variables
Diagram
AspectProcessThread
MemoryIsolated memory spaceShares memory with other threads
CreationHeavyweight (more overhead)Lightweight (less overhead)
CommunicationIPC (Inter-Process Communication)Shared memory (faster)
IsolationHigh (crash doesn’t affect others)Low (crash can affect other threads)
Context SwitchExpensive (save/restore memory)Cheaper (save/restore registers)
Data SharingDifficult (requires IPC)Easy (shared memory)
Resource UsageMore memory, more overheadLess memory, less overhead
Diagram

Context switching is when the CPU switches from executing one process/thread to another. This is crucial for understanding performance differences.

Diagram

Python provides two main approaches for concurrent execution, each with different use cases.

The GIL is a mutex (lock) that protects access to Python objects, preventing multiple threads from executing Python bytecode simultaneously.

Diagram

The GIL is automatically released during:

  • I/O operations (reading files, network requests)
  • C extension calls (NumPy, C libraries)
  • Sleep operations (time.sleep())

Example: CPU-Bound Task (GIL Limits Performance)

Section titled “Example: CPU-Bound Task (GIL Limits Performance)”

Let’s see how threading performs poorly for CPU-bound tasks:

cpu_bound_threading.py
import threading
import time
def cpu_bound_task(n):
"""CPU-intensive task"""
result = 0
for i in range(n):
result += i * i
return result
def run_with_threading():
"""Using threading - GIL limits performance"""
start = time.time()
threads = []
for _ in range(4):
thread = threading.Thread(target=cpu_bound_task, args=(10000000,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end = time.time()
print(f"Threading time: {end - start:.2f} seconds")
# Output: ~8 seconds (similar to sequential!)
if __name__ == "__main__":
run_with_threading()

Result: Threading doesn’t help for CPU-bound tasks because only one thread executes Python bytecode at a time due to GIL.

Example: CPU-Bound Task (Multiprocessing Works!)

Section titled “Example: CPU-Bound Task (Multiprocessing Works!)”

Now let’s use multiprocessing to bypass the GIL:

cpu_bound_multiprocessing.py
import multiprocessing
import time
def cpu_bound_task(n):
"""CPU-intensive task"""
result = 0
for i in range(n):
result += i * i
return result
def run_with_multiprocessing():
"""Using multiprocessing - bypasses GIL"""
start = time.time()
processes = []
for _ in range(4):
process = multiprocessing.Process(target=cpu_bound_task, args=(10000000,))
processes.append(process)
process.start()
for process in processes:
process.join()
end = time.time()
print(f"Multiprocessing time: {end - start:.2f} seconds")
# Output: ~2 seconds (4x faster on 4 cores!)
if __name__ == "__main__":
run_with_multiprocessing()

Result: Multiprocessing uses separate processes, each with its own Python interpreter and GIL, enabling true parallelism!

Example: I/O-Bound Task (Threading Works Great!)

Section titled “Example: I/O-Bound Task (Threading Works Great!)”

For I/O-bound tasks, threading works well because GIL is released during I/O:

io_bound_threading.py
import threading
import time
import requests
def fetch_url(url):
"""I/O-bound task - GIL released during network I/O"""
response = requests.get(url)
return response.status_code
def run_with_threading():
"""Using threading - works great for I/O"""
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
start = time.time()
threads = []
for url in urls:
thread = threading.Thread(target=fetch_url, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end = time.time()
print(f"Threading time: {end - start:.2f} seconds")
# Output: ~1 second (all requests in parallel!)
if __name__ == "__main__":
run_with_threading()

Result: Threading works great for I/O-bound tasks because GIL is released during network I/O operations!

Diagram

Java has a rich threading model with different types of threads and creation patterns.

Java provides two ways to create threads:

1. Implement Runnable interface (Preferred) 2. Extend Thread class (Less flexible)

classDiagram
    class Runnable {
        <<interface>>
        +run() void
    }
    
    class Thread {
        -target: Runnable
        +start() void
        +run() void
        +join() void
        +getName() String
        +getState() State
    }
    
    class MyTask {
        +run() void
    }
    
    class MyThread {
        +run() void
    }
    
    Runnable <|.. MyTask : implements
    Thread <|-- MyThread : extends
    Thread --> Runnable : uses
    MyTask --> Thread : passed to
RunnableExample.java
public class RunnableExample {
public static void main(String[] args) {
// Create task (implements Runnable)
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Task running in: " +
Thread.currentThread().getName());
// Do some work
for (int i = 0; i < 5; i++) {
System.out.println("Count: " + i);
}
}
};
// Create thread with task
Thread thread = new Thread(task, "Worker-Thread");
thread.start();
try {
thread.join(); // Wait for completion
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished");
}
}

Why Runnable is Preferred:

  • ✅ Separation of concerns (task vs execution)
  • ✅ Can extend another class (Java doesn’t support multiple inheritance)
  • ✅ More flexible (can use with thread pools, executors)
  • ✅ Better design (follows composition over inheritance)
LambdaThreadExample.java
public class LambdaThreadExample {
public static void main(String[] args) {
// Modern approach: Lambda expression
Thread thread = new Thread(() -> {
System.out.println("Task running in: " +
Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("Count: " + i);
}
}, "Lambda-Thread");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Java threads have a well-defined lifecycle with specific states:

stateDiagram-v2
    [*] --> NEW: new Thread()
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: wait for lock
    RUNNABLE --> WAITING: wait()
    RUNNABLE --> TIMED_WAITING: sleep(timeout)
    BLOCKED --> RUNNABLE: acquire lock
    WAITING --> RUNNABLE: notify()
    TIMED_WAITING --> RUNNABLE: timeout/notify
    RUNNABLE --> TERMINATED: run() completes
    TERMINATED --> [*]

Thread States:

  • NEW: Thread created but not started
  • RUNNABLE: Thread is executing or ready to execute
  • BLOCKED: Waiting for a monitor lock
  • WAITING: Waiting indefinitely for another thread
  • TIMED_WAITING: Waiting for a specified time
  • TERMINATED: Thread has completed execution
ThreadStateExample.java
public class ThreadStateExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000); // TIMED_WAITING
synchronized (ThreadStateExample.class) {
// BLOCKED if another thread holds lock
System.out.println("Thread executing");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("State: " + thread.getState()); // NEW
thread.start();
System.out.println("State: " + thread.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println("State: " + thread.getState()); // TIMED_WAITING
thread.join();
System.out.println("State: " + thread.getState()); // TERMINATED
}
}

Java: Platform Threads vs Virtual Threads (Java 19+)

Section titled “Java: Platform Threads vs Virtual Threads (Java 19+)”

Java 19 introduced Virtual Threads (Project Loom), a revolutionary approach to concurrency.

  • 1:1 mapping with OS threads
  • Heavyweight - each thread consumes ~1-2MB of memory
  • Limited scalability - typically hundreds to thousands of threads
  • Expensive context switching - OS-level scheduling
  • M:N mapping - many virtual threads mapped to fewer OS threads
  • Lightweight - each thread consumes ~few KB of memory
  • High scalability - can create millions of virtual threads
  • Efficient scheduling - JVM manages scheduling
Diagram
VirtualThreadExample.java
import java.util.concurrent.Executors;
public class VirtualThreadExample {
public static void main(String[] args) {
// Create virtual thread (Java 19+)
Thread virtualThread = Thread.ofVirtual()
.name("virtual-worker")
.start(() -> {
System.out.println("Running in virtual thread: " +
Thread.currentThread().getName());
System.out.println("Is virtual: " +
Thread.currentThread().isVirtual());
});
try {
virtualThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Using ExecutorService with virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " in thread: " +
Thread.currentThread().getName());
});
}
} // All tasks complete here
}
}

Diagram
ScenarioPythonJava
I/O-bound tasksthreading or asyncioVirtual Threads (Java 19+) or Platform Threads
CPU-bound tasksmultiprocessingPlatform Threads or ForkJoinPool
High concurrency (I/O)asyncioVirtual Threads
Simple parallelismmultiprocessingExecutorService with thread pool
Need isolationmultiprocessingSeparate processes

Diagram

Scenario: Handle multiple HTTP requests simultaneously

Python Solution:

# Use threading or asyncio for I/O-bound web requests
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
# I/O operation - GIL released
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello")
# Threading works great for I/O-bound tasks
server = HTTPServer(('localhost', 8000), Handler)
server.serve_forever()

Java Solution:

// Use Virtual Threads for high concurrency
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
ServerSocket server = new ServerSocket(8000);
while (true) {
Socket client = server.accept();
executor.submit(() -> handleRequest(client));
}
}

Scenario: Process multiple images in parallel

Python Solution:

# Use multiprocessing for CPU-bound image processing
import multiprocessing
from PIL import Image
def process_image(image_path):
# CPU-intensive operation
img = Image.open(image_path)
img = img.filter(ImageFilter.BLUR)
img.save(f"processed_{image_path}")
# Multiprocessing bypasses GIL
with multiprocessing.Pool() as pool:
pool.map(process_image, image_files)

Java Solution:

// Use ForkJoinPool for CPU-bound tasks
ForkJoinPool pool = ForkJoinPool.commonPool();
List<Future<Void>> futures = imageFiles.stream()
.map(path -> pool.submit(() -> processImage(path)))
.collect(Collectors.toList());

Pitfall 1: Using Threading for CPU-Bound Tasks in Python

Section titled “Pitfall 1: Using Threading for CPU-Bound Tasks in Python”
# DON'T: Using threading for CPU-bound tasks
import threading
def cpu_intensive():
result = sum(i*i for i in range(10000000))
threads = [threading.Thread(target=cpu_intensive) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
# No speedup! GIL prevents parallel execution
// DON'T: Creating thousands of platform threads
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
// I/O operation
makeHttpRequest();
}).start();
}
// May exhaust system resources!
  1. Choose the right tool for the task

    • I/O-bound → Threading/Async
    • CPU-bound → Multiprocessing/Process pools
  2. Use thread pools (don’t create threads manually)

    • Better resource management
    • Reuse threads (lower overhead)
  3. Understand your language’s limitations

    • Python GIL for CPU-bound tasks
    • Java platform thread limits
  4. Consider virtual threads (Java 19+)

    • Perfect for I/O-bound, high-concurrency scenarios
  5. Measure performance

    • Don’t assume threading/multiprocessing is faster
    • Profile and benchmark your code

AspectProcessThreadPython ThreadingPython MultiprocessingJava Virtual Thread
MemoryIsolatedSharedSharedIsolatedShared (lightweight)
GIL ImpactN/ALimitedYes (CPU-bound)NoN/A
Best ForIsolationI/O tasksI/O tasksCPU tasksI/O tasks (high concurrency)
ScalabilityLowMediumMediumMediumVery High
OverheadHighLowLowHighVery Low

Problem: You need to process 1000 images (CPU-intensive) and send results via HTTP (I/O). What approach would you use in Python?

Solution

Use multiprocessing for image processing (CPU-bound) and threading or asyncio for HTTP requests (I/O-bound).

import multiprocessing
import threading
import requests
def process_image(image_path):
# CPU-bound - use multiprocessing
# ... image processing ...
return processed_image
def send_result(result):
# I/O-bound - use threading
requests.post("http://api.example.com/result", data=result)
# Process images in parallel (multiprocessing)
with multiprocessing.Pool() as pool:
results = pool.map(process_image, image_files)
# Send results in parallel (threading)
threads = [threading.Thread(target=send_result, args=(r,))
for r in results]
for t in threads:
t.start()
for t in threads:
t.join()

Problem: Design a system that processes both CPU-bound and I/O-bound tasks efficiently.

Solution

Use a hybrid approach:

  • Thread pool for I/O-bound tasks (HTTP requests, database queries)
  • Process pool for CPU-bound tasks (image processing, calculations)
  • Queue to coordinate between them
import multiprocessing
import threading
from queue import Queue
# Queues for coordination
cpu_queue = Queue()
io_queue = Queue()
def cpu_worker():
while True:
task = cpu_queue.get()
if task is None:
break
result = process_cpu_task(task) # CPU-bound
io_queue.put(result)
def io_worker():
while True:
result = io_queue.get()
if result is None:
break
send_result(result) # I/O-bound
# Start CPU workers (processes)
cpu_pool = multiprocessing.Pool(processes=4)
# Start I/O workers (threads)
io_threads = [threading.Thread(target=io_worker)
for _ in range(10)]

Q1: “When would you use multiprocessing vs threading in Python?”

Section titled “Q1: “When would you use multiprocessing vs threading in Python?””

Answer:

  • Multiprocessing: For CPU-bound tasks (computation, image processing, data analysis) because it bypasses the GIL and enables true parallelism across multiple CPU cores.
  • Threading: For I/O-bound tasks (network requests, file I/O, database queries) because the GIL is released during I/O operations, allowing concurrent execution.

Q2: “How does the GIL affect Python’s threading performance?”

Section titled “Q2: “How does the GIL affect Python’s threading performance?””

Answer: The GIL (Global Interpreter Lock) allows only one thread to execute Python bytecode at a time. This means:

  • CPU-bound tasks: Threading provides no speedup (may even be slower due to overhead)
  • I/O-bound tasks: Threading works well because the GIL is released during I/O operations
  • Solution: Use multiprocessing for CPU-bound tasks to bypass the GIL

Q3: “What are the trade-offs between processes and threads?”

Section titled “Q3: “What are the trade-offs between processes and threads?””

Answer:

  • Processes: Better isolation (crash doesn’t affect others), but higher overhead, more memory usage, slower communication (IPC)
  • Threads: Lower overhead, faster communication (shared memory), but less isolation (crash can affect other threads), need synchronization for shared data

Q4: “What are Java Virtual Threads and when should you use them?”

Section titled “Q4: “What are Java Virtual Threads and when should you use them?””

Answer: Virtual Threads (Java 19+) are lightweight threads managed by the JVM:

  • Benefits: Very low memory overhead (~few KB), can create millions, efficient for I/O-bound tasks
  • Use when: High-concurrency I/O-bound scenarios (web servers, API clients, database connections)
  • Don’t use for: CPU-bound tasks (use platform threads or ForkJoinPool instead)

Now that you understand threads vs processes, continue with:

Remember: Choose the right tool for your task! Understanding when to use threads vs processes is crucial for designing efficient concurrent systems. 🚀