Free lesson · Concurrency Interview Mastery
"At the same time" is a lie - and it's the most useful lie in computing
When you start two threads, you picture them running side by side, both moving forward at the same instant. Hold that picture in your head, because I'm about to take it away from you, and the version I replace it with is the one that will keep you from freezing in an interview.
On a single CPU core, two threads do not run at the same time. They can't. A core executes one instruction at a time - that's what a core is. So what actually happens when you "run two threads" on one core? The CPU runs a slice of thread A, freezes it mid-stride, runs a slice of thread B, freezes that, jumps back to A, and so on - switching thousands of times a second. It happens so fast that from the outside it looks like both are running together. It's the same trick film uses: a strip of still frames flicked past your eyes fast enough that you see motion. There's no motion on the strip. There's no simultaneity in the core. Both are convincing illusions.
So if concurrency isn't "doing things at the same time," what is it? Here's the definition I want you to carry for the rest of this course:
Concurrency is structuring a program so it can deal with many things at once - making progress on multiple tasks over a stretch of time, whether or not they ever literally execute in the same instant.
Read that again and notice what it does not say. It doesn't say "simultaneously." It says "deal with" and "make progress." Concurrency is about how you organize work so that multiple things are in flight, not about whether two electrons move at the same nanosecond.
The mental shift: stop thinking "simultaneous," start thinking "interleaved"
This is the single most important idea in the whole subject, so I'm going to slow down on it.
The wrong mental model - threads running smoothly side by side - is dangerous because it makes concurrency bugs invisible to you. If two things just happen "together," there's nothing to reason about. But that's not what happens. What happens is that the operations from your threads get chopped into small pieces and interleaved - shuffled together into a single timeline in an order you do not fully control.
Picture two threads that each do the same simple thing: read a value, add one, write it back.
sequenceDiagram
participant Core as Single CPU Core
Core->>Core: A: read counter (sees 5)
Core->>Core: B: read counter (sees 5)
Core->>Core: A: add 1 (has 6)
Core->>Core: B: add 1 (has 6)
Core->>Core: A: write counter (now 6)
Core->>Core: B: write counter (now 6)
Note over Core: Two increments happened.<br/>Counter went from 5 to 6, not 7.
This sequence diagram shows a single CPU core executing pieces of two threads, A and B, one step at a time. First thread A reads the counter and sees the value five. Then thread B also reads the counter and also sees five, because A hasn't written anything back yet. Then A adds one to get six, and B adds one to get six. Then A writes six, and B writes six. The note at the bottom points out that two separate increments happened, but the counter only went from five to six instead of seven. One increment was completely lost because of the order in which the steps were interleaved.
Look at that timeline. Nothing happened "at the same time" - every step is on its own line, in sequence. And yet an increment vanished. Not because of magic, but because the interleaving let both threads read 5 before either wrote 6. That's the entire ballgame. Every concurrency bug you will ever meet is some interleaving you didn't expect.
Important: Train yourself to ask, about any concurrent code: "what are the possible interleavings of these steps, and is there one that breaks my assumptions?" If you can ask that question, you can reason about concurrency. If you're still thinking "they run at the same time," you can't - there's nothing concrete to reason about. The whole skill is making the invisible interleavings visible.
This is also where the animations in this course earn their keep. You just read an interleaving in that diagram. Later, you'll watch one - see the two threads step on each other in real time, slow it down, replay it, and watch the increment disappear. Reading about a lost update and watching one happen are different experiences, and the second one is the one that sticks.
Why concurrency exists at all
Let me answer a question you might be too polite to ask: if concurrency is this dangerous, why do we bother? Why not just run everything in a straight line?
Because programs spend an astonishing amount of their lives waiting. Waiting for a network response. Waiting for a disk read. Waiting for a database. Waiting for a user. While a program waits on one of those, the CPU - which could be executing billions of instructions a second - sits there doing nothing useful for that task.
Concurrency is, at its heart, the art of not wasting that waiting time. While task A is blocked waiting for the network, the CPU switches to task B and gets real work done. When the network finally answers A, the CPU can switch back. From the outside, it looks like your program is juggling a hundred things. Really, it's just refusing to stand still while any one of them waits.
Example: Think about a web server handling a thousand users. Each request involves waiting - for a database, for another service, for the disk. If the server handled them one at a time, start to finish, user number one thousand would wait forever. With concurrency, while request one waits on its database call, the server serves request two, then three, then back to one when its data arrives. No request finishes faster in raw CPU terms, but a thousand of them make progress together. That's concurrency doing exactly its job: turning dead waiting time into useful work.
So concurrency buys you two things: responsiveness (your program stays alive and reactive while individual tasks wait) and throughput (you get more total work done by filling the waiting gaps). Both come from the same trick - interleaving work so the CPU is never idle when there's something else it could do.
The context switch - the price of the illusion
Switching between threads isn't free. When the CPU stops thread A to run thread B, it has to save everything about where A was - its registers, its place in the program, its working state - and load all of B's. That save-and-load is called a context switch, and it costs real time. Not much per switch, but it adds up, and it's why "just add more threads" is not always faster.
flowchart LR
A[Running Thread A] -->|save A's state| B[Context Switch]
B -->|load B's state| C[Running Thread B]
C -->|save B's state| D[Context Switch]
D -->|load A's state| A
This flowchart shows the cost of switching between threads on a single core. The core is running thread A. To switch, it must save thread A's state, perform a context switch, then load thread B's state to start running B. Later, to switch back, it saves B's state, does another context switch, and reloads A's state. The diagram emphasizes that every switch between threads carries the overhead of saving one thread's state and loading another's.
Warning: A common beginner mistake is assuming more threads always means more speed. Past a point, the CPU spends so much time context-switching between threads that it does less useful work, not more. This is called thrashing. The right number of threads depends on what they're doing - threads that mostly wait on I/O can vastly outnumber your cores, while threads doing heavy computation should roughly match your core count. We'll come back to this judgment in "When NOT to Go Concurrent."
One line of code is often several operations
I keep saying the increment is "really three steps," so let me make that concrete, because it's the hinge the entire course swings on. When you write this:
counter = counter + 1; // or counter++
the CPU doesn't do it in one indivisible move. It does roughly three:
1. READ counter from memory into a register (register = 5)
2. ADD 1 to the register (register = 6)
3. WRITE the register back to memory (counter = 6)
Three separate operations. And the context switch we just talked about can happen between any two of them. That's the gap. Thread A does step 1 (reads 5), then the scheduler switches to thread B, which does all three (reads 5, adds, writes 6). Then A resumes from where it paused - it still has 5 in its register - adds 1 to get 6, and writes 6 over B's 6. Two increments, one survivor.
Important: Whenever you look at concurrent code, mentally expand the innocent-looking single lines into their real machine steps and ask: "if a switch happened between any two of these, in either thread, would anything break?" An operation that can't be safely interrupted partway is called atomic.
counter++is not atomic, even though it looks like one thing. Most concurrency bugs are non-atomic operations that the author assumed were atomic.
When does the switch actually happen?
You might be wondering who decides when the CPU pauses one thread to run another. There are two models, and the difference matters.
Preemptive scheduling is what modern operating systems use. The scheduler can interrupt a running thread at almost any point - between any two machine instructions - and hand the core to another thread. The running thread doesn't get a say and doesn't get a warning. This is powerful (no single thread can hog the CPU) but it's also exactly why concurrency is hard: the switch can land in the middle of your read-modify-write, and you can't predict where.
Cooperative scheduling is the older, gentler model: a thread runs until it voluntarily yields control. The switch only happens at points the thread chooses. This makes some reasoning easier - you know you won't be interrupted mid-operation - but one badly-behaved task that never yields freezes everything.
Note: Most threads you'll write run under preemptive scheduling, so you must assume a switch can happen anywhere. Interestingly, some concurrency models you'll meet later - like async/await and goroutines at certain points - reintroduce cooperative-style yielding at well-defined spots, which is part of why they can be easier to reason about. For now, the safe default assumption is: the rug can be pulled at any instant, so never assume a multi-step operation will finish uninterrupted.
Watching the interleaving step by step
To cement it, here's the exact interleaving that loses an update, laid out as a trace so you can point at the precise moment it goes wrong:
Example: Two threads each run
counter++once. Start value: 5. Expected end value: 7.
Step Thread Action A's register B's register counter 1 A read counter 5 – 5 2 B read counter 5 5 5 3 A add 1 6 5 5 4 B add 1 6 6 5 5 A write back 6 6 6 6 B write back 6 6 6 Final counter: 6, not 7. The damage was done at step 2 - B read 5 before A had written its 6. From there both threads worked from a stale value and one increment was doomed. No error, no crash, no exception - the program runs cleanly and quietly gives the wrong answer. That silence is what makes these bugs dangerous, and why we learn to see the interleaving rather than trust that code "looks right."
The flip side: change the interleaving - let A finish all three steps before B starts - and you get the correct 7. Same code, same inputs, different schedule, different answer. A program whose correctness depends on the scheduler's whims is a program with a race condition, which is exactly what we build toward.
What this looks like in an interview
Here's why this seemingly philosophical lesson is interview gold. Interviewers frequently open with something deceptively simple - "what is concurrency?" - and they are listening for the interleaving model. A candidate who says "it's when things run at the same time" has just revealed they're operating on the wrong mental model, and the interviewer now knows exactly where to push to make them stumble.
Interview: When asked what concurrency is, don't say "doing things at the same time." Say something like: "Concurrency is structuring a program so multiple tasks can make progress over the same period. On a single core they're not literally simultaneous - the CPU interleaves their operations, switching between them. The reason it matters is that the interleaving order isn't fully under my control, which is the root of every concurrency bug." That answer signals, in three sentences, that you actually understand the model. Mentioning interleaving is the tell that separates people who've read about concurrency from people who understand it.
Strong follow-up to be ready for: "If it's just interleaving on one core, why does it cause bugs?" Your answer: because operations that look atomic in your code - like
counter++- are actually multiple steps under the hood (read, modify, write), and another thread can slip in between those steps.
That last point - that one line of code is often several operations, and a switch can happen between them - is the seed of the entire next half of this course. counter++ looks like one indivisible thing. It isn't. It's a read, an add, and a write, and the gap between them is where the lost update in our first diagram lived.
What you've actually got now
You came in picturing threads running side by side. You're leaving with something more useful and more honest: threads as streams of operations that get interleaved into a single timeline, in an order you don't control, with a context-switch cost every time the CPU changes its mind about which one to run. That model is the foundation everything else is built on. Hold onto "interleaved, not simultaneous" - when a concurrency problem confuses you later, returning to that phrase will almost always show you the way in.
Next, we'll sharpen a distinction that trips up even experienced engineers and that interviewers love to probe: concurrency versus parallelism. They are not the same thing, and knowing exactly how they differ is a small answer that signals a lot.
Quiz: On a single CPU core, two threads incrementing a shared counter can lose an update. What is the root cause? A) The two threads literally execute at the exact same instant and collide B) The CPU is too slow to handle two threads C) The increment is multiple steps (read, modify, write), and the threads' steps interleave so both read the old value before either writes D) Single cores cannot run more than one thread
Answer: C. Nothing runs simultaneously on one core. The bug comes from interleaving: the increment is really read-modify-write, and the steps from the two threads shuffle together so both read the old value before either writes the new one.