JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

User Threads, Kernel Threads, and the Models Between

You know what a thread is and that concurrency is not parallelism. Now meet the two places a thread can be managed — quietly inside your program, or out in the open by the kernel — and the three ways to wire them together, each with its own honest trade-off.

Who keeps the thread's to-do list?

In guide 1 you met a thread as a single flow of control inside a process, sharing the code, data, and heap but keeping its own stack, registers, and program counter. In guide 2 you learned that running several of them gives you concurrency, and only real cores give you parallelism. One question was quietly skipped: who actually keeps track of all these threads and decides when each one runs? It turns out there are two possible bookkeepers, and which one you pick changes almost everything about how your threads behave.

The first bookkeeper is a library living inside your own program, in user space — these are user-level threads. Picture a manager with a private to-do list who quietly switches between jobs at their own desk and never tells the head office which task they are on. The head office (the kernel) sees one busy employee. The library keeps its own little table of threads, each with a saved stack and registers, and when one should yield to another it does the swap itself, in user mode, with no system call at all. Because nothing ever crosses into the kernel for an ordinary switch, creating and switching these threads is astonishingly fast and cheap.

The second bookkeeper is the operating system itself — these are kernel-level threads. Now every worker is registered with the head office, which assigns desks and decides who works when. The kernel keeps a record for each thread, and its scheduler chooses among threads, not just among processes, when deciding what each CPU core runs next. The price is that creating a thread, switching between threads, and synchronizing them all go through the kernel via system calls, so each operation is heavier than the purely in-process switch of a user-level thread. Almost every general-purpose OS today — Linux, Windows, macOS — provides kernel-level threads as the foundation the user-facing APIs are built on.

The two costs that decide everything

Before the models make sense, you need to feel the two opposing costs in your bones. A user-level switch is cheap because it stays out of the kernel: the library just saves one thread's registers and loads the next, the way you flip from one bookmark to another in the same book — no context switch into the kernel, no crossing the user-kernel boundary. A kernel-level switch is more expensive because every thread operation is a system call: a trip across that boundary, with the kernel saving and restoring state. So far user-level threads look like a clean win on speed.

But cheapness hides two traps, and both come from the same fact: the kernel cannot schedule a thread it cannot see. First, parallelism. If the kernel sees your whole process as one schedulable thing, it can only ever place it on one CPU core at a time — so a hundred user-level threads still run on a single core, taking turns. That is concurrency, yes, but never true parallelism, no matter how many cores the machine has. Second, blocking. If any one user-level thread makes a blocking system call (say, a slow disk read), the kernel parks the whole process, because as far as it knows there is nothing else to run — and every other thread freezes along with it, even though they had useful work to do.

Three ways to wire them together

A model is simply a rule for how many user threads map onto how many kernel threads. The many-to-one model funnels many user threads onto a single kernel thread — like a whole team sharing one office phone line. Switching among the user threads is all done in user space, fast and cheap, but the model inherits both user-level traps in full force: only one core can ever be used, and one blocking call stops everyone. It has largely been abandoned for general use; it survives mainly underneath pure user-level threading and the simplest green-thread libraries.

The one-to-one model goes to the opposite extreme: every user thread gets its very own kernel thread, paired one for one — every worker with a dedicated phone line. Because the kernel sees each thread individually, two threads of the same process can run on two different cores at the very same instant (real parallelism!), and when one thread blocks on a slow read, the kernel simply runs another, so the rest carry on. This directness is exactly why it is the model used by Linux, Windows, and most modern systems. The catch is the bill: creating a user thread now means creating a kernel thread, a heavier kernel operation, and each kernel thread consumes kernel resources, so systems often cap how many you may create. Spawning one thread per tiny task is wasteful — which is precisely the problem the thread pool of the next guide solves.

  MANY-TO-ONE            ONE-TO-ONE              MANY-TO-MANY

  U  U  U  U            U   U   U   U           U U U U U U U U
   \ | | /              |   |   |   |            \ \ \ | / / / /
    \| |/               |   |   |   |             \ \ \|/ / / /
     (K)                K   K   K   K              K    K    K
      |                 |   |   |   |              |    |    |
   [1 core only]     [runs on many cores]    [many U on a few K]

  U = user thread    K = kernel thread (what the OS can schedule)
The three multiplexing models. Count the kernel threads (K): that is exactly what the OS can place on cores. Many-to-one has just one, so it can never use a second core.

Many-to-many: the best of both, and why it is hard

The many-to-many model (also called M:N) tries to keep the good parts of both extremes. Imagine a call center with fifty agents but only eight outside lines: a supervisor multiplexes the agents onto the lines as lines free up, so the company gets the responsiveness of fifty workers without paying for fifty lines. Here a user-space scheduler multiplexes thousands of cheap user threads onto a modest pool of kernel threads — say, one per CPU core. You can create as many user threads as you like, run them across multiple cores, and avoid the whole process freezing: when one user thread blocks in the kernel, the runtime runs another user thread on a different kernel thread. A common variant, the two-level model, even lets you pin a particular user thread directly to its own kernel thread when you need to.

Here is where we must be honest, because the textbook picture and the real world disagree. On paper many-to-many is the ideal. In practice it is genuinely hard to implement well: the cooperation needed between the user-space scheduler and the kernel scheduler is intricate, and several operating systems built it, struggled with it, and then dropped it in favor of plain one-to-one. So the model's biggest real success is not in the kernel at all — it lives in modern language runtimes, where an M:N scheduler is layered on top of ordinary one-to-one kernel threads. Go's goroutines are the famous example, routinely juggling hundreds of thousands of tasks over a handful of kernel threads. You will meet those lightweight threads in the next guide.

The API you actually touch: pthreads

You will rarely choose a model by hand; you reach for a thread library, and the model is whatever that library and your OS provide underneath. The most important library on Unix-like systems is pthreads (POSIX threads). Like every car putting the steering wheel and pedals in the same place, pthreads fixes a common set of function names and behaviors so the same threaded C program compiles and runs on Linux, macOS, and the BSDs. It is a specification, not one particular implementation — each OS ships its own library that obeys it. On Linux a pthread maps one-to-one to a kernel thread, so it gives you true parallelism out of the box.

The core calls are few and learnable. You call pthread_create() to start a new thread running a function you name; later you call pthread_join() to wait for that thread to finish and collect its result; pthread_exit() ends the calling thread. Two honest cautions worth carrying forward. First, an API is a contract about names and behavior — it is not the same thing as a system call. Pthreads is a library that may use system calls underneath, and that API-versus-system-call boundary is a distinct idea you will keep meeting. Second, pthreads hands you the tools to share data between threads but takes no responsibility for using them correctly.

Concretely, you might write pthread_create(&t, NULL, work, arg) to launch the function work() on a new thread while the main thread keeps running alongside it, then pthread_join(t, NULL) to wait for that thread to finish. The same two lines compile and behave the same on Linux and macOS — that portability is the whole point of a standard. But that second caution is the cliff this whole rung is walking toward.

That cliff is shared mutable state. The moment you have two threads sharing the process's memory, you can write to the same variable from both, and pthreads will let you do it with no complaint. If two threads touch the same data at the same time without coordinating, you get a race condition and your program can produce wrong or random answers. (One escape hatch worth knowing: data that should never be shared can live in thread-local storage, where each thread gets its own private copy and there is simply nothing to protect.) Making genuine sharing safe is the hard, central subject of the final guide in this rung.