JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Threads vs Processes

Guide 1 separated concurrency from parallelism. Now meet the two ways an operating system actually runs more than one thing at once — processes, each in its own private world, and threads, which share a single world. Getting this boundary crisp is what makes everything later in this rung (races, locks, deadlocks) make sense rather than feel like magic.

Two kinds of "more than one at a time"

From the process rung you already know what a process is: a running program with its own address space — its own code, heap, stack, and globals — that no other process can touch. When you ran fork(), the child got a copy of everything, so a variable in one was invisible to the other. That isolation is a feature: a crash in one process cannot corrupt another. But isolation has a cost, and that cost is the reason threads exist.

A thread is a second (or third, or hundredth) thread of control — its own program counter and its own stack — running inside one process and sharing that process's single address space with the other threads. Picture a process as one apartment. Forking gives you a second, separate apartment with its own copy of the furniture. Spawning a thread instead invites another person to live in the same apartment: they get their own bed (their own stack), but the kitchen, the bookshelf, and every global is shared furniture that anyone can rearrange. That single difference — separate worlds versus one shared world — is the whole distinction, and almost everything else follows from it.

What is shared, what is private

Be exact here, because the shared/private split is the source of every concurrency bug and every concurrency win. Threads in the same process share the heap (so a pointer to a malloc()'d block is valid in every thread), the global and static variables, the open file descriptors, and the program code itself. What each thread keeps private is small but vital: its own stack (so its local variables and call frames are its own), its own program counter and registers, and a little thread-local storage. Everything reachable through a shared pointer is shared; everything that lives only as a local on a thread's own stack is private.

A picture helps. Imagine one process as a single block: the top region holds the shared things — the code, the globals and statics, the heap built by malloc(), and the file-descriptor table — and every thread sees exactly that same region. Below it sits one private strip per thread: thread 1 has its own stack, program counter, registers, and TLS; thread 2 has its own; thread 3 its own. Three threads, then, means one shared upper region and three independent lower strips. When you reason about a thread bug, the first question is always which region a variable lives in — the shared top, where two threads can collide, or a private strip, where they cannot.

This is exactly why threads are powerful and dangerous in the same breath. Powerful, because two threads can cooperate on one big data structure in the shared heap with no copying and no message-passing — they just both hold a pointer to it. Dangerous, because that same shared data structure is now shared mutable state: if both threads write it at unpredictable moments, you get a data race, the precise hazard guides 4 and 5 of this rung are built around. The shared address space that makes threads efficient is the very thing that makes them need synchronization.

Why threads are cheaper than processes

Because threads share one address space, the kernel does far less work to make one. Creating a process (think fork-then-exec) means setting up a whole new address space — page tables, regions, the lot — even with copy-on-write helping. Creating a thread reuses the existing address space and mostly just allocates a new stack and a small kernel bookkeeping structure. As a rough order of magnitude, spawning a thread is often ten times cheaper than spawning a process, and a thread's footprint is dominated by its stack (commonly a default of around 8 MiB of address space reserved, though only touched pages cost real memory).

Switching between them is cheaper too. A context switch between two threads of the same process does not have to swap address spaces, so the MMU keeps its page tables and the TLB (the cache of recent address translations) need not be flushed. Switching between two processes usually does flush much of that, and refilling it afterwards costs real time. So threads win on creation cost, on memory, and on switch cost — which is why a web server handling thousands of connections often uses threads rather than a process per connection.

How you make one: pthreads and std::thread

On Unix the classic interface is POSIX threads, usually written pthreads. You hand pthread_create() a pointer to a function and an argument, and it starts a new thread running that function concurrently with the caller; the new thread ends when that function returns (or calls pthread_exit()). The parent then calls pthread_join() to wait for the thread to finish and collect its result — the thread-world cousin of wait() for processes, which you met in the process rung. Without joining (or detaching), the thread is left dangling, the rough equivalent of a zombie.

#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    long id = (long)arg;
    printf("thread %ld running\n", id);
    return NULL;
}

int main(void) {
    pthread_t t;
    int rc = pthread_create(&t, NULL, worker, (void *)1L);
    if (rc != 0) {
        fprintf(stderr, "pthread_create failed: %d\n", rc);
        return 1;
    }
    pthread_join(t, NULL);   /* wait for the worker to finish */
    return 0;
}
A minimal pthreads program. Note rc is checked: pthread_create() returns an error number directly (not via errno), and a program that ignores it will use an uninitialized pthread_t. Compile with: gcc -O2 -Wall main.c -pthread

Higher-level languages wrap the same OS thread in a friendlier object. C++ gives you std::thread, whose constructor starts the thread and whose .join() waits for it; Rust's std::thread::spawn() returns a handle you .join() likewise, and — being Rust — the borrow checker enforces at compile time that data you hand to another thread is shared safely, turning a class of data race bugs into build errors. These are conveniences over the same kernel primitive, not a different mechanism: underneath, each is still one OS thread sharing one address space, with all the sharing hazards intact. The syntax changes; the physics does not.

Choosing, and what comes next

So which do you reach for? Reach for separate processes when isolation matters more than sharing: when a crash or a security breach in one part must not take down or read the others (a browser puts each site in its own process for exactly this reason), or when the pieces barely need to talk. Reach for threads when the work is tightly coupled around shared data and you want speed and low overhead — a parallel sort over one big array, or many short-lived request handlers touching one in-memory cache. The honest rule of thumb: threads when you want to share state, processes when you want to be protected from sharing it.

One honest caveat before we move on: threads and processes are not the only model, and "more threads" is not automatically "more speed". As guide 1 stressed, concurrency is not parallelism — eight threads on one core still take turns, and work that is one unsplittable chain gains nothing from extra threads. And processes can still cooperate without sharing an address space, through the inter-process communication mechanisms — pipes, sockets, shared memory — that a later rung explores. Threads are one powerful tool, not the only one.

You now hold the boundary this whole rung pivots on: a process is a private world, a thread is a shared world, and threads are cheaper precisely because they share. The next guide, Creating Threads with pthreads, makes the pthread_create() / pthread_join() skeleton above concrete and runnable. Then guides 4 and 5 turn to the dark side of all that sharing — why `count++` from two threads can quietly lose updates (the race condition), and why a mutex is the tool that fixes it.