The core mental model
Every concurrency mechanism you'll ever use is really just a different answer to one question: what unit of execution am I creating, and what does it share with the thing that created it? There are three answers you need to know cold, and the entire rest of this roadmap is built on getting this taxonomy exactly right before you touch synchronization.
| Unit | Owns its own... | Shares... | Created/switched by |
|---|
| Process | Address space, file descriptor table, heap, security context | Nothing by default (needs explicit IPC) | The OS kernel |
| Thread | Program counter, register set, stack | Address space, open files, heap — everything else in the process | The OS kernel (for kernel threads) |
| Coroutine | A saved "resume point" (its own stack or continuation) | Everything a thread shares, plus the OS thread itself | A language runtime / scheduler, in user space |
The pattern across the row is decreasing isolation, decreasing creation cost, increasing count you can afford. A process might cost megabytes and milliseconds to spin up; a thread costs kilobytes and microseconds; a coroutine can cost as little as a few hundred bytes, which is why some runtimes comfortably run millions of them.
Processes: isolation as the default
A process is a running program with its own private virtual address space — its own view of memory, its own file descriptors, its own everything, enforced by the OS via page tables. Two processes cannot corrupt each other's memory by accident; if one crashes, the kernel reclaims its resources and the other is unaffected. This isolation is exactly why processes are expensive: creating one means the kernel has to build a fresh address space and page tables, and any communication between two processes has to go through explicit, kernel-mediated channels (pipes, sockets, shared memory segments) instead of just reading a variable.
The classic interview-relevant detail here is fork(): on POSIX systems, a process can only be created by duplicating an existing one (copy-on-write, so it's cheaper than it sounds), and the child immediately diverges from the parent. This is the mechanism underneath most other process-creation APIs, and "how many processes does this fork()-calling snippet create" is a genuinely common systems-interview warm-up — the answer is always a function of how many existing processes execute each subsequent fork() call, not a fixed multiplier.
Threads: the shared-everything alternative
A thread is a path of execution within a process. Every process starts with exactly one thread (conventionally called the main thread), and that thread can create more. All threads in a process share the address space, heap, and open file handles — which is precisely why threads are cheap to create and fast to communicate between (just read/write a shared variable), and precisely why they're dangerous: nothing stops two threads from reading and writing the same memory at the same instant unless you explicitly coordinate them. That coordination problem is the entire subject of the next topic on this roadmap.
What a thread does have that's private: its own program counter, its own register set, and its own stack. That's the minimum state the OS needs to save and restore in order to pause one thread and run another — which is exactly what a context switch is (covered in depth in this topic's third subtopic).
Kernel-level vs. user-level threads
This is the detail that separates a surface-level answer from a senior-level one: not all "threads" are known to the OS kernel.
- Kernel-level threads (KLTs) are threads the OS scheduler directly knows about and schedules onto CPU cores. This is what
pthread_create (and, transitively, java.lang.Thread, Python's threading.Thread, etc.) gives you on modern systems — a 1:1 model, one kernel thread per user-visible thread.
- User-level threads (ULTs) are threads multiplexed by a library entirely in user space; the kernel sees only the one (or few) underlying kernel threads carrying them. Switching between ULTs can be extremely fast (no kernel trap needed) — but if one ULT makes a blocking system call, the entire underlying kernel thread blocks, potentially starving every other ULT multiplexed onto it, unless the runtime specifically works around this (as Go's goroutine scheduler and Java's virtual threads do, by detecting blocking calls and moving other work onto a spare kernel thread).
Most mainstream languages today use the 1:1 model for their "plain" threads, which is exactly why the next section exists.
Coroutines: cooperative, not preemptive
A coroutine is a unit of execution that can voluntarily suspend itself and later be resumed from exactly where it left off — without ever requiring a full kernel-level context switch, because it never needed its own OS thread in the first place. The scheduling here is cooperative: a coroutine keeps running until it explicitly yields control (typically at an await/suspend point), rather than being forcibly preempted by a timer interrupt the way a kernel-scheduled thread is.
This is the mechanism behind Python's async/await, JavaScript's async functions, Kotlin's coroutines, and (conceptually) Go's goroutines. A single OS thread can host thousands of suspended coroutines because each one's "saved state" is just a small continuation/stack, not a full kernel thread control block. The trade-off: a coroutine that never yields (e.g., it runs a tight CPU-bound loop with no await) will starve every other coroutine sharing its thread — there's no timer interrupt coming to rescue you, unlike with preemptively-scheduled OS threads.
Why interviewers care
This subtopic rarely produces a coding question on its own — its job is to be the vocabulary you're graded on for the rest of the interview. When you say "I'll use a thread pool here," a strong interviewer is silently checking whether you understand that you're trading isolation for shared-memory speed, and whether you'd reach for coroutines instead if the workload is I/O-bound with high concurrency and low per-task CPU cost. Getting this framing right before you write a line of synchronization code is what separates "knows the Lock API" from "understands why the Lock API needs to exist."
Pitfalls and interview gotchas
- Confusing "lightweight" with "free." Threads and coroutines reduce cost relative to processes, but shared mutable state is not free — it's a liability that has to be actively managed (that's the entire rest of this roadmap).
- Assuming all threads are kernel-scheduled. Green threads / ULTs exist, and "a blocking call on one thread stalls everything" is a real failure mode worth naming if asked about a specific runtime.
- Treating coroutines as "just async threads." A coroutine that never yields will monopolize its thread indefinitely — there's no preemption to save you, which is the opposite failure mode from an OS thread hogging a core (which the scheduler will eventually interrupt).
- Forgetting that
fork() duplicates, it doesn't share. A child process gets a copy of the parent's memory (copy-on-write) — mutating a "shared" variable after fork() does not propagate back to the parent, unlike a genuinely shared thread-local variable.
Where this goes next
Concurrency vs. Parallelism (next) sharpens exactly what you gain by having more than one thread or process: sometimes it's genuine simultaneous execution, and sometimes it's only the appearance of it via fast switching on a single core. Then Scheduling & Context Switching gets precise about the mechanism — and the cost — of that switching, for both threads and processes.