Module 2 · Unit 4 · Lesson 1 of 5
Why Threads?
The motivation for splitting execution from the process. What a thread is, how it relates to a process, what it shares and what it owns, and the trade-offs the abstraction imposes.
01 Introduction
Unit 3 developed the process as the fundamental unit of an operating system. A process owns an address space, a set of open files, credentials, and a machine state. This bundling is convenient for isolation: a bug in one process cannot corrupt another. But it is inconvenient for a different reason — everything inside a process runs as a single thread of control, one program counter, one stack, one instruction stream.
Consider what that means for the workloads modern systems actually run. A web server wants to handle many requests at once but share a common cache and log file. A video decoder wants to split frames across CPU cores but write into one output buffer. A spreadsheet wants to recalculate in the background while the user continues to edit. A build system wants to compile independent files in parallel while linking them into one executable. Each of these workloads wants many streams of execution that share the process's resources rather than duplicate them.
The abstraction that makes this possible is the thread: a unit of execution inside a process. Threads are conceptually a refinement of the process model — they split the "unit of resource ownership" from the "unit of execution". This lesson explains why that split is useful, what threads actually are, what they share and what they own, and what costs they impose. Lessons 4.2 through 4.5 then examine the implementation models, the multicore implications, and the POSIX programming interface.
Prerequisites
This lesson builds directly on Unit 3. In particular, the process concept (Lesson 3.1), the state model (Lesson 3.2), and the PCB (Lesson 3.3) are the foundation for everything that follows. If any of those is unclear, review them before continuing.
02 Learning objectives
By the end of this lesson you will be able to
- Define a thread.
- Explain why threads exist.
- Compare threads and processes.
- Identify thread benefits.
- Analyse thread costs.
This lesson contributes to U4.1 and to course learning outcomes CLO2 and CLO8.
03 Required reading
- OSTEP — Chapter 26, Concurrency: An Introduction. Read the introduction for the motivation and the framing of concurrency as one of the three "easy pieces" of operating systems.
- Operating System Concepts — Chapter 4, Sections 4.1 and 4.2. The classical presentation of the thread concept, its benefits, and its relationship to the process.
-
Love, R. — Linux Kernel Development,
3rd ed., Chapter 3, the section on threads. How Linux
distinguishes threads from processes internally (the
task_structview). -
Linux man pages:
man 7 pthreads(the POSIX threads overview),man 3 pthread_create,man 2 clone(the Linux-specific call that underlies both fork and thread creation).
Reading strategy
Read OSTEP's introduction first for the motivation, then Silberschatz Section 4.1–4.2 for the formal treatment of the thread concept. Love's section is short and shows the concrete Linux view, which is useful background for the interactive activity at the end of this lesson.
04 Core content
4.1 The process revisited
In Lesson 3.1, the process was defined as "a program in execution". Implicitly, that definition bundles two separate concepts into a single abstraction:
- A unit of resource ownership. The process owns an address space, a set of open file descriptors, credentials, signal handlers, and other resources. These are granted by the kernel and are the process's "identity" from the OS's point of view.
- A unit of execution. The process has a program counter, a stack, a register set, and a stream of instructions that it executes. This is the "thread of control" through the program.
For a program with a single thread of control — a shell, a compiler, a command-line tool — this bundling is invisible. There is exactly one unit of execution inside the process and nothing is lost. The scheduler chooses between processes, context switches save and restore one register state, and the model from Unit 3 applies directly.
For a program that wants to do several things at once — a web server, a GUI application, a parallel computation — the bundling becomes restrictive. To get a second stream of execution, the program has to create a whole new process with its own address space, its own file descriptors, and its own copy of any state it needs to share. That is expensive, and it is inconvenient: the two processes cannot easily see each other's data, so any communication must go through pipes, shared memory, or some other mechanism the OS provides.
The thread abstraction is what happens when we relax the bundling: keep the process as the unit of resource ownership, but allow multiple units of execution to live inside it.
4.2 What a thread is
A thread is a sequence of instructions executing within a process. Every process has at least one thread; some systems allow many threads per process. All threads of a process share the process's address space, its open files, its signal handlers, and its credentials — that is, everything the process owns. Each thread has its own program counter, its own register set, its own stack, and its own thread ID.
The definition is best understood by contrasting it with the process from Unit 3. A process is a container: it holds memory, files, and identity. A thread is a stream of execution that runs inside that container. The container can be shared by several streams; the streams themselves are independent.
4.3 What threads share and what they own
The list of shared versus per-thread resources is the single most important thing to internalise about threads. It determines everything about how threads behave, what can go wrong, and what the operating system must do to manage them.
| Shared by all threads | Owned by each thread |
|---|---|
| Code segment (text) | Program counter |
| Data segment | Register set |
| BSS segment | Stack pointer and stack |
| Heap | Thread ID (TID) |
| Open file descriptors | Thread-local storage |
| Signal handlers | Signal mask |
| Credentials (UIDs, GIDs) | Error number (errno) |
| Working directory, root | Scheduling priority (in some systems) |
| Page table (address space) | — |
Two consequences of this table deserve emphasis.
- Shared memory means direct communication but also races. When thread A wants to pass data to thread B, it writes into the shared heap and B reads it. No pipe, no message, no kernel involvement. But the same property means that two threads writing to the same location at the same time is a hazard. Race conditions, locks, and synchronization — the subject of Unit 6 — exist precisely because threads share memory.
- Shared resources mean the kernel tracks fewer objects per thread. A thread does not have its own page table or its own credential set. The kernel's per-thread state is much smaller than its per-process state. This is one reason thread creation and switching are cheaper than process creation and switching.
4.4 The four classic benefits
The textbook literature identifies four benefits of threads over processes for the same workloads. Each corresponds to a specific cost that threads avoid.
Responsiveness
A process with a long-running computation can hand that computation to a background thread and remain interactive on the main thread. A word processor can spell-check the document in the background while the user types. A web server can process an expensive request on a worker thread while continuing to accept new connections. Without threads, the alternative is either a freeze or a more complex event-driven design.
Resource sharing
Threads share memory by default. Two threads that need to
coordinate over a shared data structure can simply read and
write the structure directly. Two processes that need the
same coordination must set up shared memory with
mmap, or communicate through pipes or sockets.
The shared-memory model is both faster and more convenient
for many workloads.
Economy
Creating a thread is cheaper than creating a process: the kernel does not duplicate a page table, copy file descriptor tables, or set up a new address space. Switching between threads of the same process is cheaper than switching between processes: the memory map stays the same, so the TLB does not need to be flushed or retagged, and the cache remains useful. On a system that creates and destroys many short tasks, the cumulative savings can be substantial.
Scalability
On a multicore system, threads within one process can execute simultaneously on different cores. The threads share the same address space, so a parallel algorithm that reads and writes shared data structures scales naturally. A process-based design can also achieve parallelism, but it must either duplicate the data or explicitly share it through shared memory, which is more expensive and error-prone.
4.5 The costs of threads
The benefits come at a price. Four costs appear repeatedly in practice, and understanding them is essential for using threads well.
- Loss of isolation
- Because threads share the address space, a bug in one thread can corrupt data that another thread depends on. A stray write to an out-of-bounds array index, a use-after-free of a shared pointer, a null dereference through a shared structure — all of these affect every thread in the process. In a multi-process design, the same bug would crash one process without affecting the others.
- Synchronization burden
- Every shared mutable resource must be protected, and every access must be checked. The programmer must reason about interleavings, choose appropriate primitives, and avoid the subtle bugs — race conditions, deadlocks, livelocks — that arise when concurrency is not managed carefully. This is the single largest source of complexity in multithreaded software, and it is the subject of Units 6 and 7.
- Debugging difficulty
- Bugs in concurrent code are notoriously difficult to reproduce. A race condition may manifest only one run in a million, only under specific loads, or only on specific hardware. A tool like GDB, which works by pausing and stepping through code, changes the timing that produced the bug in the first place, often making it disappear. This is why multithreaded code requires a different debugging discipline than single-threaded code.
- Per-thread resource consumption
- Each thread needs its own stack, which on Linux defaults to about 8 MB of virtual address space. A program that creates thousands of threads can consume tens of gigabytes of address space — a significant cost, even if the stacks are sparsely used in physical memory. Modern language runtimes that support very large numbers of concurrent tasks (Go, Erlang) use much smaller stacks and grow them dynamically, precisely to avoid this cost.
4.6 When a process is the better choice
Threads are not always the right answer. There are three contexts in which a process-based design is preferable.
- When isolation matters more than sharing. If a task must not corrupt another task's data — because the tasks are untrusted, or because the consequences of corruption are severe — a process boundary provides containment that threads cannot. Chrome's site-isolation model, in which each tab is a separate process, is a deliberate choice of isolation over the economy of threads.
- When the tasks do not share state. If two tasks operate on disjoint data and communicate only through well-defined messages, the sharing advantage of threads disappears. In that case, processes provide equivalent performance with stronger isolation.
- When the workload is I/O-bound and the OS provides a scalable alternative. Event-driven architectures with non-blocking I/O can handle tens of thousands of concurrent connections on a single thread, without the memory cost or synchronization burden of one thread per connection. This is the design used by Nginx, Node.js, and many modern network servers.
In practice, modern systems use a hybrid: a small pool of worker processes, each running a small pool of threads. The processes provide coarse isolation; the threads provide parallelism and resource sharing within each process. This pattern appears in PostgreSQL (process per connection), Apache prefork (process per connection), Apache worker (thread per connection), and most production web servers.
4.7 A concrete comparison: two server designs
To make the trade-offs concrete, compare two ways to build a simple web server.
Process per request
The listener accepts a connection and forks a child to handle it. Each child has its own address space, its own copy of the response cache (if it was set up before the fork, the child sees a copy-on-write version), and its own log buffer.
- Crash impact: a bug in one handler kills only that child.
- Shared state: any shared state — a session table, a metrics counter — must be handled through shared memory or through a separate service.
- Cost per request: a fork is expensive relative to a thread creation, though modern copy-on-write makes it cheaper than it once was.
- Scale: thousands of processes are possible but each consumes a full address space's worth of kernel bookkeeping.
Thread per request
The listener accepts a connection and spawns a thread to handle it. All threads share the response cache, the session table, and the log file.
- Crash impact: a crash in any thread takes down the entire server.
- Shared state: the cache and session table are simply shared variables, protected by locks.
- Cost per request: much lower than a fork; a thread creation is on the order of microseconds.
- Scale: tens of thousands of threads are possible in principle, but the stack memory cost and the synchronization overhead of protecting the shared cache limit practical concurrency.
Neither design is universally better. The process model is safer and simpler to reason about; the thread model is faster and more natural when sharing is the point. Modern high-performance servers often use a hybrid — process-based isolation with thread-based workers inside each process — because each model contributes what it does best.
4.8 Threads and the Unit 3 model
It is worth pausing to see how threads fit into the process model from Unit 3. Three relationships matter.
- A thread is not a process. The five-state model from Lesson 3.2 describes states of a thread of control, not of a process. When we said "the process is running", we meant "one of its threads is running". Once we introduce threads, the process's state is not a single value — it is the collection of its threads' states.
-
The kernel tracks both. A process is the unit
of resource ownership: the kernel holds a page table, a file
descriptor table, and credentials for the process as a whole.
A thread is the unit of scheduling: the kernel holds a
program counter, a register set, and a kernel stack for each
thread. On Linux, both are represented by the same underlying
task_struct, which is why threads and processes look similar at the kernel level — but the resource-sharing semantics differ. - Context switches between threads are cheaper. This was foreshadowed in Lesson 3.5. Because threads share the memory map, a thread switch does not need to change the page-table base, flush the TLB, or invalidate the cache. Only the registers and stack pointer change. This is one of the main performance arguments for threads over processes, and it becomes concrete in Lesson 4.5.
4.9 Preview: the threading models
Once a process can contain multiple threads, the next question is where the thread abstraction is implemented. Two broad answers exist, and they lead to the different models examined in Lessons 4.2 and 4.3.
- User-level threads. The thread library runs entirely in user space; the kernel sees only one thread of control per process. Switching between user threads does not involve the kernel, so it is very fast. But a blocking system call in any user thread blocks the entire process, and user threads of one process cannot run on multiple cores.
- Kernel-level threads. The kernel knows about each thread and schedules them independently. Blocking calls block only the calling thread; threads can run on multiple cores. The cost is that thread operations require kernel involvement, making them slower than pure user-thread operations.
Modern general-purpose systems — including Linux and Windows — use the kernel-level model, because it solves both the blocking and multicore problems and the performance penalty is modest. User-level threads survive in specialised contexts and as a research model, and their ideas influence modern lightweight concurrency approaches such as Go's goroutines.
05 Worked example
To make the concepts concrete, look at the same task implemented two ways: once with processes and once with threads.
Summing an array: processes vs. threads
The task: sum the elements of a large array of integers, splitting the work across four workers.
Version 1 — four processes
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#define N 40000000L
#define WORKERS 4
long *data; /* in shared memory */
long *result; /* one slot per worker */
int main(void) {
data = mmap(NULL, N * sizeof(long), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
result = mmap(NULL, WORKERS * sizeof(long), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
for (long i = 0; i < N; i++) data[i] = 1;
size_t per = N / WORKERS;
for (int w = 0; w < WORKERS; w++) {
pid_t pid = fork();
if (pid == 0) {
/* Child: sum its chunk */
long sum = 0;
for (size_t i = w * per; i < (w + 1) * per; i++) sum += data[i];
result[w] = sum;
_exit(0);
}
}
for (int w = 0; w < WORKERS; w++) wait(NULL);
long total = 0;
for (int w = 0; w < WORKERS; w++) total += result[w];
printf("total = %ld\n", total);
return 0;
}
Three things to notice. First, the data must live in
shared memory (created with mmap and
MAP_SHARED), because otherwise each child would
have its own private copy after fork. Second,
each child writes to its own slot in result, so
no synchronization is needed. Third, the results must be
combined by the parent after all children have finished, using
the wait calls.
Version 2 — four threads
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N 40000000L
#define WORKERS 4
long data[N];
long result[WORKERS];
size_t per = N / WORKERS;
void *worker(void *arg) {
int w = *(int *)arg;
long sum = 0;
for (size_t i = w * per; i < (w + 1) * per; i++) sum += data[i];
result[w] = sum;
return NULL;
}
int main(void) {
for (long i = 0; i < N; i++) data[i] = 1;
pthread_t t[WORKERS];
int ids[WORKERS];
for (int w = 0; w < WORKERS; w++) {
ids[w] = w;
pthread_create(&t[w], NULL, worker, &ids[w]);
}
for (int w = 0; w < WORKERS; w++) pthread_join(t[w], NULL);
long total = 0;
for (int w = 0; w < WORKERS; w++) total += result[w];
printf("total = %ld\n", total);
return 0;
}
The threaded version is shorter and simpler. The data array is
an ordinary global — all threads see it directly. The result
array is also shared. Each thread writes to its own slot, so
no lock is needed. The threads are joined, then the results
are combined. There is no mmap, no
MAP_SHARED, no explicit interprocess
coordination.
Comparing the two
| Aspect | Processes | Threads |
|---|---|---|
| Data shared by | mmap(MAP_SHARED) | Ordinary globals |
| Sync required | No (disjoint slots) | No (disjoint slots) |
| Startup cost | Fork per worker; page table copied | Thread create per worker; no page table copy |
| Kernel memory per worker | Full PCB + page tables | Small task_struct + kernel stack |
| Address space | Shared only for the explicitly shared region | Fully shared |
| Bug in one worker | Kills that process only | Kills the whole program |
This example shows the essential trade-off in a single contrast. The threaded version is more convenient and cheaper at startup; the process version provides stronger isolation. Which one is correct depends on what the program needs. A standalone batch sum can use threads. A web server that processes untrusted content from many clients might prefer processes.
Measuring the difference
Build both versions and time them:
gcc -Wall -Wextra -g -O2 -pthread -o sum-threads sum-threads.c
gcc -Wall -Wextra -g -O2 -o sum-procs sum-procs.c
$ time ./sum-procs
total = 40000000
real 0m0.42s
$ time ./sum-threads
total = 40000000
real 0m0.31s
The thread version is faster here for two reasons: the worker creation cost is lower, and the shared memory access is direct rather than through a mapped region that the kernel must handle specially. On a larger workload with more workers, the difference grows.
06 Interactive activity
Activity · observe threads on a live Linux system
Open a terminal in your course VM and work through the following. The goal is to connect the abstract concept of a thread to the concrete representation the kernel exposes.
-
See how many threads your shell has.
A shell is single-threaded. Every process has at least one thread; the$ echo $$ 4321 $ cat /proc/4321/status | grep Threads Threads: 1Threadsfield counts how many. -
Find a multithreaded process. Most graphical
applications, databases, and language runtimes are
multithreaded. Find one and inspect its thread count:
The$ ps -eLo pid,tid,nlwp,comm | awk '$3 > 1' | head -20nlwpcolumn shows the number of light-weight processes (threads) per process. Which processes have the most threads on your VM? -
List the threads of one process.
Each line is one thread of the process, with its own$ ps -L -p <PID> -o pid,tid,stat,pcpu,commtid(thread ID) and its own state. Note that the threads share the samepidbut have distincttids. -
Examine a thread's per-thread data. The
kernel exposes one directory per thread under
/proc/<pid>/task/:
Each subdirectory is named after a thread ID and contains the same files (stat, status, etc.) that a single-threaded process would have. These are the per-thread state — the program counter, registers, and stack information for that particular thread.$ ls /proc/<PID>/task/ -
Compare the shared and per-thread data.
Choose a multithreaded process and, for one of its threads,
compare what is shared:
The address space is one map — shared by all threads. The per-thread stat file shows the CPU time that thread has accumulated on its own.$ cat /proc/<PID>/maps | head # shared by all threads $ cat /proc/<PID>/task/<TID>/stat | awk '{print $14, $15}' # per-thread CPU time -
Write a simple multithreaded program.
Create a small C program with two threads, each printing a
message a few times, and observe its behaviour:
Compile with#include <stdio.h> #include <pthread.h> void *worker(void *arg) { const char *name = arg; for (int i = 0; i < 5; i++) printf("%s: iteration %d\n", name, i); return NULL; } int main(void) { pthread_t t1, t2; pthread_create(&t1, NULL, worker, "A"); pthread_create(&t2, NULL, worker, "B"); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0; }gcc -Wall -Wextra -g -pthread -o demo demo.cand run it several times. Does the interleaving of A and B outputs vary between runs? -
Observe the threads of the demo program.
Run the program, and while it is running, use
ps -L -C demo -o pid,tid,stat,commfrom another terminal. How many threads do you see?
Reflect: which of the four benefits from section 4.4 is most visible in the demo program? Which of the four costs would become apparent if the two threads wrote to the same counter without synchronization?
07 Knowledge check
Check your understanding
Question 1
List three things that threads in the same process share and three things that each thread owns privately.
Show answer
Question 2
Why is a thread context switch cheaper than a process context switch?
Show answer
Question 3
A program uses ten threads to increment a shared counter one million times each. The final value is less than ten million. Explain what went wrong and why threads are responsible.
Show answer
Question 4
Give one workload where threads are clearly the right choice and one where processes are clearly the right choice, and explain the reasoning for each.
Show answer
Question 5
A programmer creates 5,000 threads in a program and finds that the program consumes tens of gigabytes of virtual address space even though most threads are idle. Explain why.
Show answer
pthread_attr_setstacksize) or
to use a runtime with much smaller stacks, such as Go's
goroutines. This is a concrete instance of the
"per-thread resource consumption" cost from section 4.5.
08 Unit discussion
Discussion prompt
Some modern language runtimes — Go, Erlang, Rust's async
ecosystem — provide their own lightweight concurrency
abstractions on top of operating-system threads, and
discourage or prohibit direct use of the OS-level interface.
Go's goroutines and channels, Erlang's processes and message
passing, and Rust's async tasks all layer a
different concurrency model over the same underlying
primitives.
Is this a sign that the OS-level thread is the wrong abstraction for modern concurrent programming — too heavy, too error-prone, or both — or is it a sign that different languages have different needs and the OS thread remains the right foundation? Argue for whichever view you find more persuasive, using one concrete example of a workload where the higher-level abstraction clearly wins and one where it does not.
Post your response to the Unit 4 discussion forum and reply to at least two classmates. Supports CLO2 and CLO8.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lab 3. Aim to spend 45–60 minutes on it.
- Enumerate shared and per-thread state. Without consulting the table in section 4.3, write two columns — "shared" and "per-thread" — and fill them with everything you remember. Then compare against the table. Which did you miss, and why?
-
Find the threads on your VM.
Run
ps -eLo pid,tid,nlwp,comm | awk '$3 > 1'and identify the three processes with the most threads. For each, do you know why it would need that many threads? Look up the process name if you do not recognise it. - Build both versions of the array sum. Compile the process-based and thread-based versions from the worked example. Time each on your VM. Are the results similar to those in the worked example, or does your VM show a bigger or smaller difference?
- Deliberately create a race. Modify the thread-based version so that all four threads increment a single shared counter (rather than writing to separate slots). Compile it without any locking and run it. Compare the reported total to the expected value. Run it several times — is the discrepancy always the same? This is a preview of Unit 6.
-
Measure the stack cost. Write a program
that creates 1,000 threads, each of which sleeps for a
minute. While it is running, examine the process's virtual
memory size:
How large is it? Does it match the expectedgrep VmSize /proc/<PID>/status1000 × 8 MB = 8 GBof virtual address space? - Reflect. In your own words, describe one workload that you personally use that would be significantly worse if implemented with processes instead of threads, and explain why. Then describe one that would be worse with threads instead of processes, and explain why.
10 Summary
- A thread is a unit of execution inside a process. Every process has at least one thread; some have many.
- Threads share the code, data, heap, open file descriptors, signal handlers, and credentials of the process. Each thread owns its program counter, register set, stack, thread ID, and thread-local storage.
- The four classic benefits of threads: responsiveness (a background task does not freeze the main thread), resource sharing (threads share memory directly), economy (creating and switching threads is cheaper than for processes), and scalability (threads run in parallel on multicore systems).
- The four classic costs of threads: loss of isolation (a bug in one thread affects all), synchronization burden (every shared access must be protected), debugging difficulty (race conditions are non-deterministic and often disappear under debuggers), and per-thread resource consumption (each thread needs its own stack, often 8 MB of virtual address space by default).
- Threads are not always the right answer. When isolation matters more than sharing, when the tasks do not share state, or when an event-driven design would fit better, a process-based or asynchronous design may be preferable. Modern systems often combine both: worker processes, each containing a small pool of threads.
-
On Linux, both processes and threads are represented by the
same underlying
task_struct. A process is a group of threads that share an address space, file descriptor table, and credentials.
Looking ahead
Lesson 4.2 examines user-level threads — threads that are managed entirely in user space, invisible to the kernel. Their speed and their limitations motivate the kernel-level model of Lesson 4.3, which is what Linux and most modern systems actually implement.
11 References
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 26.
Available online at
ostep.org. - Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 4, Sections 4.1–4.2.
- Love, R. Linux Kernel Development, 3rd ed., Chapter 3 (Process Management), the section on threads. Addison-Wesley, 2010.
-
Linux man-pages:
man 7 pthreads,man 3 pthread_create,man 2 clone,man 5 proc(the/proc/<pid>/task/directory). - Butenhof, D. R. Programming with POSIX Threads, Chapter 1. Addison-Wesley, 1997. (The classical introduction to the thread model from the programmer's perspective.)