AlgoInsight

Free lesson · Concurrency Interview Mastery

The two ways to run code at once - and why the choice decides where your bugs live

Easy10 min readconcurrencymultithreadinginterview-prep

To get concurrency, you need more than one stream of execution. There are two things you can spin up to get those streams: processes and threads. They sound similar and people use them loosely, but the difference between them is one of the most consequential decisions in concurrent design - because it determines whether your concurrent streams share memory, and shared memory is where nearly every concurrency bug you'll ever debug comes to live.

Let me build both up from the ground.

A process is an isolated world

When you launch a program - your browser, your text editor, a Python script - the operating system creates a process for it. A process is a self-contained world: it gets its own private chunk of memory, its own view of the resources, its own everything. Critically, one process cannot reach into another process's memory. If your browser process has a variable at some memory address, your editor process has no way to read or write it. They're walled off.

That isolation is the defining feature of a process. It's why one program crashing usually doesn't take down the others - each lives in its own sealed room. The operating system is the landlord, and the walls between rooms are solid.

Note: This isolation is also a security boundary. Your password manager runs in its own process partly so that a bug in some other application can't simply read its memory. Process isolation isn't just about stability - it's a wall the OS enforces in hardware.

A thread lives inside a process - and shares its memory

Now, within a single process, you can have multiple threads. A thread is a stream of execution - a sequence of instructions being run - and a process can have many of them running concurrently. Here's the part that matters most: threads inside the same process share that process's memory. They live in the same room. They can all see and modify the same variables, the same objects, the same heap.

This sharing is a double-edged sword, and understanding both edges is the whole point of this lesson.

The good edge: threads can communicate almost for free. If thread A wants to hand data to thread B, it just... writes it to a shared variable, and B reads it. No copying, no message passing, no overhead. They're in the same memory, so sharing is instant.

The bad edge: because they share memory, they can step on each other. Two threads modifying the same variable with no coordination is exactly the lost-update bug from the first lesson. Shared mutable memory is the root soil of concurrency bugs. Almost everything in the rest of this course - locks, atomics, the whole toolbox - exists to manage the consequences of threads sharing memory.

flowchart TD
    subgraph P1["Process 1 (isolated memory)"]
        H1[Shared Heap: objects, variables]
        T1A[Thread A<br/>own stack] --> H1
        T1B[Thread B<br/>own stack] --> H1
        T1C[Thread C<br/>own stack] --> H1
    end
    subgraph P2["Process 2 (isolated memory)"]
        H2[Separate Heap]
        T2A[Thread A<br/>own stack] --> H2
    end
    P1 -. cannot access .-> P2
This diagram shows two processes, each with isolated memory. Inside process one there is a shared heap containing objects and variables, and three threads - A, B, and C - each with their own private stack, all pointing at and sharing that single heap. Process two has its own separate heap and one thread with its own stack. A dashed line between the two processes is labeled "cannot access," showing that process one cannot reach into process two's memory. The key idea is that threads within a process share the heap but each keep a private stack, while processes are walled off from each other entirely.

Notice the detail in that diagram: threads share the heap but each gets its own stack. The stack holds a thread's local variables and its place in the call chain - that's private to each thread, which is why local variables inside a function are generally safe from other threads. The heap holds the shared objects - and that's where coordination becomes your problem. When you hear "thread-safe," it's almost always about protecting access to shared heap data.

The tradeoffs, side by side

So when do you reach for processes, and when for threads? It comes down to a few axes.

Dimension Processes Threads
Memory Isolated - private to each Shared within the process
Communication Expensive (must pass messages, copy data) Cheap (shared variables)
Crash blast radius One crash stays contained One thread crashing can take down the whole process
Creation cost Heavier - OS sets up a whole new memory space Lighter - reuses the process's memory
Context switch cost Higher (must swap memory mappings) Lower (same memory space)
Where bugs live Fewer shared-state bugs by construction Shared-state bugs (races, deadlocks) are the default risk

Read that table as a single story: threads buy you cheap, fast sharing at the price of dangerous shared state. Processes buy you safety and isolation at the price of expensive communication. That tradeoff is the whole decision.

Example: A web browser is a great illustration. Modern browsers run each tab (or each site) in its own process, not just a thread. Why pay the heavier cost? Because isolation means one tab crashing or getting compromised doesn't take down or expose the others. The browser deliberately trades cheap communication for safety, because for tabs, safety wins. Meanwhile, within a single tab's process, many threads handle rendering, network, and scripting - there, cheap shared-memory communication wins. The same application uses both, each where its tradeoff fits.

The communication problem

Because processes don't share memory, getting two processes to talk requires real machinery - collectively called inter-process communication, or IPC: pipes, sockets, shared-memory segments the OS explicitly sets up, message queues. All of it involves copying data across the wall or asking the OS to poke a hole in it. It works, but it's heavier than threads just reading the same variable.

This is why, when you need lots of fast back-and-forth between your concurrent streams, threads are the natural choice - the communication is free because there's nothing to communicate across. And it's why, when you need strong isolation and your streams barely talk, processes are the natural choice - you're not paying the communication tax often anyway.

Warning: Don't reach for threads just because they're cheaper. The cheapness is exactly what makes them dangerous - cheap sharing means accidental sharing, and accidental sharing of mutable state is a race condition waiting to happen. Every shared variable between threads is a liability you have to actively protect. Sometimes the "expensive" isolation of processes is the cheaper choice once you count the debugging hours you didn't spend chasing a once-in-a-million race.

A note on the language layer

How this shows up depends on your language, and we'll go deep on each in the next module, but one preview matters here because it surprises people. In Python, the Global Interpreter Lock means threads don't give you true parallelism for CPU-bound work - so Python programmers often reach for multiple processes to use multiple cores, accepting the communication cost to get real parallelism. In Java, Go, C++, and C#, threads do run on multiple cores, so the calculus is different. The process-vs-thread decision isn't purely abstract; your language's runtime tilts it. Keep that in your back pocket; we'll unpack the Python case properly when we get there.

Seeing the shared memory in code

Let me show you the gift and the curse in a few lines, because reading about "shared memory" is abstract until you watch two threads touch the same variable.

class Counter {
    int count = 0;                 // lives on the shared heap

    void increment() { count++; }  // every thread sees the SAME count
}

// Two threads, one Counter object:
Counter c = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) c.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) c.increment(); });
t1.start(); t2.start();
t1.join();  t2.join();
System.out.println(c.count);       // Expected 200000. Actual: usually less.

Both threads reference the same Counter object on the heap. That's the gift - t1 and t2 communicate through shared memory with zero ceremony, no copying, no messages. And that's the curse - because count++ is the non-atomic read-modify-write from lesson one, the two threads interleave and lose increments, so the total comes out under 200,000 and changes from run to run. If these had been two separate processes, each would have its own count, the interference would be impossible - but then they couldn't share a single total at all without explicit IPC. That tradeoff, in nine lines.

Note: Notice the local loop variable i is not shared - each thread has its own copy on its own stack. Only count, reached through the shared heap object, is contended. This is the stack-vs-heap split from the diagram made concrete: locals are private, heap objects are shared. When you hunt for a race, the heap-reachable shared state is where you look.

Why we pool threads instead of spawning them

The table listed "creation cost" as heavier for processes and lighter for threads - but "lighter" is not "free." Spinning up a brand-new thread still asks the OS to allocate a stack and register the thread with the scheduler, which costs real time and memory. If a busy server created a fresh thread for every one of millions of requests and threw it away after, the creation and teardown overhead alone would crush it, and the memory for thousands of simultaneous stacks would exhaust the machine.

The practical answer, which you'll see everywhere in real systems, is the thread pool: create a fixed set of threads once, then feed them a queue of tasks. A thread finishes one task and immediately picks up the next instead of dying. You pay the creation cost a handful of times at startup, not once per task, and you cap how many threads can exist so memory and context-switching stay bounded.

Tip: When an interviewer asks how you'd handle "thousands of incoming requests," the expected move is almost never "spawn a thread per request." It's "use a bounded thread pool" - reusing threads and capping concurrency. We give thread pools a full lesson later, but the reasoning starts here: thread creation is cheaper than process creation, but it's still expensive enough that you reuse threads rather than minting them per task.

A middle ground: lightweight threads

Before we hit the interview wrap, one more idea, because it bridges the cost gap and it's increasingly what interviewers expect you to know exists. The threads we've described so far are OS threads - the operating system knows about each one, schedules it, and gives it a sizeable stack. That's why they're not free: a machine can comfortably run thousands of OS threads, but not millions.

So runtimes invented a lighter-weight kind: green threads (also called virtual threads, or in Go, goroutines). These are threads the language runtime manages, not the OS. The runtime multiplexes many of them onto a small pool of real OS threads. Each one is cheap - a tiny stack that grows as needed - so you can spin up millions of them. When a green thread blocks on I/O, the runtime quietly parks it and runs another on the same OS thread, filling the waiting time without an expensive OS-level context switch.

Note: This is why Go programs casually launch hundreds of thousands of goroutines, and why Java added virtual threads - they collapse the old tension between "I want lots of concurrent tasks" and "OS threads are too expensive to have lots of." You get the cheap-sharing, easy-communication benefits of threads at a fraction of the per-thread cost. We cover goroutines and virtual threads properly in the language module; for now just register that the process-vs-thread tradeoff has a third option, and "use virtual threads / goroutines" is sometimes the right answer to "how would you handle a huge number of concurrent connections."

The mental model: green threads don't change the isolation story - they still share memory like OS threads, so they carry all the same race-condition risk. What they change is the cost story, making it cheap to have an enormous number of concurrent streams. Sharing's gift and curse both remain; you just get a lot more of them for less.

The interview angle

"What's the difference between a process and a thread?" is one of the most common opening concurrency questions there is. A weak answer lists surface facts ("a process is a program, a thread is part of one"). A strong answer goes straight to the consequence that matters.

Interview: Lead with the memory model, because that's what everything else follows from: "A process has its own isolated memory; threads within a process share the process's memory. That one difference drives the rest - threads communicate cheaply through shared variables but are exposed to race conditions on that shared state, while processes are isolated and safer but have to communicate through heavier IPC. Threads also share the heap but each have their own stack."

Likely follow-ups and the moves that score:

  • "Why are threads more dangerous?" → Shared mutable memory. Two threads touching the same data without coordination is the default path to a race.
  • "When would you use processes over threads?" → When isolation matters more than communication cost - fault isolation, security boundaries, or to sidestep something like Python's GIL for CPU-bound parallelism.
  • "What's private to a thread?" → Its stack - local variables and call state. The heap is shared. Knowing this crisply signals real understanding.

Tip: A fast way to decide in practice: start by asking "do these streams of work need to share a lot of data quickly?" If yes, threads (shared memory makes it cheap). "Do they need to be isolated for safety or fault-tolerance, and barely talk?" If yes, processes (isolation is the whole point, and the communication tax is rare). Most of the time the honest answer is threads - which is precisely why most of this course is about taming the shared-memory risk that threads bring.

What you've got now

The whole lesson collapses to one sentence worth memorizing: processes are isolated, threads share memory, and that shared memory is both the gift and the curse of threading. The gift is free communication. The curse is that every shared variable is a bug waiting for the wrong interleaving. Most of this course is about managing the curse without giving up the gift.

Next, we zoom in on a single thread and ask: across its life, what states does it move through? A thread isn't always running - it's often waiting, blocked, or asleep, and knowing exactly which is the difference between diagnosing a stuck program in seconds versus hours.

Back to Concurrency Interview Mastery