Threads and processes are the two fundamental units of concurrent execution on a modern OS. Both allow multiple code paths to run in overlapping time, but they make opposite tradeoffs between sharing and isolation. Picking the wrong one leads to either hard-to-debug race conditions (threads) or unnecessary IPC overhead (processes). In 2026, the choice is more nuanced than ever with virtual threads, goroutines, and async runtimes all competing for the same use cases.
What changed in 2026
- Java Virtual Threads shipped in Java 21 LTS and are now in widespread production use — they give the ergonomics of blocking threads at the cost of goroutines (~1 KiB).
- Python's
multiprocessing became easier with the ProcessPoolExecutor and improved pickle performance, while threading remains limited by the GIL for CPU work.
- Container isolation replaced process isolation for many microservice architectures — each service runs as a container (separate process namespace, cgroup), not just a separate process.
- WebAssembly components model introduced a lightweight isolation primitive between WASM modules that is cheaper than a process fork.
Core difference: what is shared
| Resource |
Same thread |
Different threads (same process) |
Different processes |
| Stack |
No (each thread has its own) |
No |
No |
| Heap |
Yes |
Yes |
No (copy-on-write on fork) |
| File descriptors |
Yes |
Yes |
Inherited on fork; separate after exec |
| Address space |
Yes |
Yes |
No |
| CPU registers |
No |
No |
No |
| Signal handlers |
Yes |
Yes |
No |
Process creation
// Fork: create a near-identical copy of the current process
pid_t pid = fork();
if (pid == 0) {
// Child process
execv("/usr/bin/my-worker", argv);
exit(1);
} else if (pid > 0) {
// Parent process — pid is the child's PID
int status;
waitpid(pid, &status, 0);
}
fork() uses copy-on-write: the child gets the same virtual pages as the parent, but they are only physically copied when either side writes to a page. Spawning a child process is cheap if it exec's immediately.
Thread creation
// POSIX thread
pthread_t tid;
pthread_create(&tid, NULL, worker_fn, arg);
pthread_join(tid, NULL);
Thread creation is ~10–100× cheaper than fork (no address space copy). Communication between threads is direct shared memory — fast but requiring synchronization.
When to use threads
- Shared mutable state is required — threads communicate via shared memory; processes need IPC (pipes, sockets, shared memory).
- Low per-task overhead matters — goroutines and virtual threads take this further, but threads are appropriate in C/C++ for balanced workloads.
- I/O-bound work in Java/.NET — the thread pool handles thousands of blocking I/O calls efficiently.
When to use processes
- Fault isolation — a crash in one worker process does not bring down the main process. Used by Chrome (one renderer process per tab), Nginx (worker processes), and Gunicorn.
- CPU-bound Python work —
multiprocessing.Pool spawns separate Python interpreters, each with its own GIL, enabling true CPU parallelism.
- Security sandboxing —
seccomp and process namespaces (used by Chrome's sandbox, container runtimes) only work at process boundaries.
from concurrent.futures import ProcessPoolExecutor
import os
def cpu_task(n):
return sum(i * i for i in range(n))
with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
results = list(pool.map(cpu_task, [10_000_000] * 8))
Goroutines and virtual threads
Both reduce the cost of "a unit of concurrent execution" far below an OS thread:
| Primitive |
Stack size |
Creation time |
Scheduler |
| OS thread |
1–8 MiB |
~10 µs |
OS kernel |
| Java Virtual Thread |
< 1 KiB |
< 1 µs |
JVM (Project Loom) |
| Goroutine |
2–8 KiB (grows) |
< 0.5 µs |
Go runtime |
| Async coroutine |
~0 KiB (state machine) |
Negligible |
Event loop |
// Go: spawn 100,000 goroutines — common and cheap
for i := 0; i < 100_000; i++ {
go func(n int) {
result := doWork(n)
_ = result
}(i)
}
// Java 21: virtual threads — blocking API, goroutine cost
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
int n = i;
executor.submit(() -> doWork(n));
}
}
IPC mechanisms (process-to-process communication)
| Mechanism |
Latency |
Throughput |
Use case |
| Shared memory (mmap) |
~100 ns |
Very high |
High-performance IPC on same machine |
| Unix domain socket |
~1 µs |
High |
Local services |
| Pipe |
~2 µs |
Moderate |
Simple parent-child data |
| TCP socket |
~10–100 µs |
Moderate |
Cross-host, general purpose |
| Message queue |
~10–50 µs |
High |
Async decoupled workers |
How to pick
- Python CPU work? Processes via
ProcessPoolExecutor or multiprocessing.Pool.
- Python I/O work?
asyncio with aiohttp/httpx — avoid both threads and processes.
- Java/Kotlin server? Java 21 Virtual Threads for blocking-style code; reactive/async if you need backpressure.
- Go? Goroutines for everything; channels for communication.
- C/C++? Threads with mutexes for shared state, or processes for isolation. Consider Rust instead.
- Node.js CPU work?
worker_threads (threads) or child_process.fork() (processes).
Common mistakes
Using threads for CPU-bound work in Python. The GIL limits all threads to one at a time for Python bytecode execution. Switch to multiprocessing.
Sharing state across processes without proper IPC. Regular Python objects cannot be passed directly between processes (they are in separate heaps). Use multiprocessing.Queue, Pipe, or shared memory explicitly.
Not joining threads or processes. Leaving background threads or zombie processes uncollected leaks OS resources. Always join() or use a context manager.
Thread pool starvation. If all pool threads are blocked on long I/O, new tasks queue up indefinitely. Either size the pool for the expected blocking factor or switch to async.
Ignoring thread safety in Django / Flask. WSGI runs each request in a thread or process (depending on the server). Any module-level mutable state (caches, connection pools) must be thread-safe.
What to skip
os.fork() in multithreaded Python — fork in a multithreaded process copies only the calling thread; other threads disappear, leaving locks potentially held, causing deadlocks.
- Thread-local storage as a caching mechanism — it works but leads to subtle bugs when threads are reused from a pool with stale state.
- Processes for sub-millisecond tasks — process spawn (~1 ms on Linux) dominates the work cost; use threads or async for fine-grained tasks.
FAQ
What is a zombie process?
A process that has exited but whose exit status has not been collected by the parent (waitpid). The OS keeps a small metadata entry. Too many zombies can exhaust the process table.
Can threads be faster than async?
For CPU-bound work on multi-core hardware, yes — threads (or processes) achieve actual parallelism. Async is single-threaded and cannot parallelize CPU computation.
What does "thread-safe" mean?
A function or data structure is thread-safe if it behaves correctly when called concurrently from multiple threads without external synchronization. The caller does not need to lock around it.
How does the OS schedule threads vs processes?
The kernel schedules threads (the fundamental unit of scheduling). A process is a container for threads. A single-threaded process has one schedulable unit; a multi-threaded process has many.
Where to go next