One juggler is not three jugglers
In the previous guide you met the thread — a flow of control inside a process, sharing the code, data, and heap with its siblings while keeping its own stack and registers. Now we tackle the single biggest source of muddled thinking about threads. People say a program 'runs things in parallel' when they really mean it 'does several things at once' in the loose, everyday sense. Those are two different ideas, and pretending they are the same will make every later topic harder. So let's separate them carefully.
Picture a single juggler keeping three balls in the air. At any instant exactly one ball is in their hand; the other two are mid-flight. The juggler is not touching three balls at once — they touch one, toss it, snatch the next, toss it, and the rapid switching creates the illusion of three things happening together. That is concurrency: several tasks in progress, interleaved over time, structured so they can make progress without waiting for each other to finish. Now picture three jugglers standing side by side, each with one ball. At this very instant, three hands are touching three balls — genuinely, physically, at the same moment. That is parallelism.
Why one core can still feel busy: time-slicing
How does a single core run a music player, a download, and your typing all 'at the same time'? It doesn't — not really. The kernel hands the core to one thread for a tiny slice of time, then performs a context switch to the next, then the next, cycling round so fast that to a human it looks seamless. This is the time-sharing idea from the very first rung, now applied to threads. The switching is real work that produces no output of its own, but it buys the appearance of many tasks advancing together.
Let's make it concrete. Suppose three threads — A, B, C — each want to run, and we have one core that hands out slices of, say, 10 milliseconds in plain round-robin order. Trace the timeline below. At no point are two threads running together; the core is always doing exactly one thing. Yet because each thread gets a turn many times per second, all three make steady progress, and a slow disk read inside A doesn't freeze B and C — when A blocks, the kernel simply gives the core to whoever is ready. That is concurrency earning its keep on a single core.
ONE CORE, three threads, 10 ms slices (round-robin): time -> 0 10 20 30 40 50 60 70 ms core: [ A ][ B ][ C ][ A ][ B ][ C ][ A ][ B ]... Never two at once. The core does ONE thing at a time. Fast switching => the ILLUSION of A, B, C together. TWO CORES, same three threads (true parallelism): time -> 0 10 20 30 ms core 0: [ A ][ A ][ C ][ C ] core 1: [ B ][ C ][ B ][ A ] At t=5 ms, A AND B are BOTH executing. Really.
Parallelism needs real hardware
Here is the honest, important limit. Concurrency is something you design into your program; parallelism is something the hardware grants you. If your laptop has four cores, then at most four threads can be truly executing at the same instant — no software trick raises that ceiling. Write a program with a thousand threads on a single-core machine and you get plenty of concurrency and exactly zero parallelism; every thread still takes turns on that one core. The thousand threads can be a perfectly good design (they let slow tasks wait without blocking fast ones), but they will not finish a pile of pure number-crunching any faster than one thread would.
This is why the reasons for using threads split into two camps. One reason is responsiveness and clean structure: keep the user interface alive while a long task chugs along in the background. That benefit is pure concurrency — it helps even on a single core, because the point is to not get stuck waiting. The other reason is raw speed on a multicore machine: split one big computation across cores so the wall-clock time drops. That benefit is parallelism — and it appears only when the hardware can actually run threads side by side.
Two flavours of parallelism
When you do reach for parallelism, it tends to arrive in one of two shapes. In data parallelism, the same operation runs over different chunks of data at the same time — like four cooks each peeling one quarter of the same mountain of potatoes. To double the brightness of a huge image, hand the top half to one core and the bottom half to another; both run identical code on different pixels. In task parallelism, different operations run at the same time — like one cook chopping while another simmers and a third plates. A web server might decompress a file on one core while encrypting a response on another; the jobs differ, yet they overlap in time.
Both flavours still ride on threads, and both are bounded by your core count. And here is a sobering truth even when the hardware is generous: not every job speeds up neatly. If the parts of a task depend on each other, or there's a stubborn slice that must run alone, adding cores yields shrinking returns. The portion of work that is inherently sequential sets a hard floor on how fast the whole thing can go, no matter how many cores you throw at it. Parallelism is powerful, but it is not magic, and 'twice the cores' rarely means 'twice as fast'.
The price of sharing: a first warning
Sharing is exactly what makes threads cheap and powerful — siblings see the same heap, so passing data between them costs nothing. But that same shared mutable memory is where things get dangerous, and the danger looks different in our two worlds. Under true parallelism, two threads on two cores can read and write the same variable at literally the same instant, and their updates can stomp on each other. Even under mere concurrency on one core, a context switch can land in the middle of a half-finished update, so the next thread sees a value that is neither the old one nor the new one.
Consider two threads that each run the innocent-looking line 'count = count + 1' on a shared counter that starts at 5. We hope for 7. But that single line is really three steps under the hood: read count into a register, add one, write the register back. Now imagine an unlucky interleaving: thread one reads 5; before it writes anything back, thread two also reads 5; each adds one to get 6 in its own register; each writes 6. Final value: 6, not 7 — one increment simply vanished. This is a race condition — a bug whose outcome depends on the unlucky timing of interleaving — and it is the central villain of the rungs ahead. The trap is that the bug only appears on the bad interleaving, so the code looks fine in testing and fails mysteriously in the wild. For now, just hold the warning: the moment two threads share mutable state without coordination, correctness is no longer guaranteed.
Putting it together
So the picture has two axes that beginners must keep apart, and this distinction between concurrency and parallelism is worth tattooing on your memory. Concurrency is about structure: organizing a program as cooperating flows of control that can be in progress together, which the kernel realizes by interleaving threads on whatever cores exist. Parallelism is about execution: actually running threads at the same instant, which only multiple cores can deliver. A well-designed concurrent program scales up gracefully when you give it more cores — but it stays correct, and stays useful, even on one.
Two threads that never appear in our pictures are the scheduler's deeper machinery and the variety of thread implementations — should each user thread map to its own kernel thread, or should many share a few? Those choices decide how much parallelism your concurrency can actually cash in. That is exactly where the next guide goes, when we open up user threads, kernel threads, and the multiplexing models between them.