MOFAKH.COM
← Back to profile
Concurrency in C#

Threads: the real unit of execution

Oct 13, 202615 min readWritten

Everything so far scheduled whole processes. But one program does many things at once without starting new programs — a server handling many requests, a player updating the clock while audio runs. It does that with threads. This part defines the OS thread properly at last: one process running many threads, each with its own stack but a shared heap, cheaper to switch than a process, and the direct foundation for the thread pool, Task, and async.

The whole path has been building to this. Part 9 switched between processes; Part 10 drew the line between concurrency and parallelism and warned that "thread" means two things. Now the software thread — the one every remaining part depends on — gets defined for real. A thread is what actually runs a program, and a single process can have many of them sharing its memory. Once that is solid, the thread pool, Task, and async are small steps on top.

What a thread is

A thread is a single path of execution through a program — the thing that actually runs instructions, one after another, following the fetch-decode-execute loop (Part 3). Earlier parts quietly assumed exactly one of these per process; this part lifts that assumption.

The precise way to see a thread, using everything built so far: a thread is captured by its own program counter (which instruction it is on), its own stack and base pointers (which call frame it is in), its own set of register values, and its own stack of frames (Part 7). That bundle is a thread — a position in the code plus the call stack and register state to support it.

Diagram
a thread =  program counter  (where it is in the code)
          + register values   (its in-flight values)
          + its own stack of frames  (its chain of active calls)

When Part 6 said "a process includes a share of CPU and its own program counter while it runs," that running-ness was really a thread. The process is the container (memory + resources); the thread is the active runner inside it. A process does not execute — its threads do.

One process, many threads

A process starts with one thread — the main thread, which begins running Main (Part 5). But a process can create more threads, and this is the key move of the whole part: those extra threads run inside the same process, sharing its memory, each executing its own path through the code at the same time.

Diagram
PROCESS  (one shared memory space)
+-- code segment   (shared by all threads)
+-- data segment   (shared)
+-- heap           (shared)
+-- Thread 1  -> its own stack, own registers, own position in the code
+-- Thread 2  -> its own stack, own registers, own position in the code
+-- Thread 3  -> its own stack, own registers, own position in the code

Each thread walks the code independently. Two threads can be in different methods at the same moment, or even running the same method with different data. The scheduler (Part 9's context switching, now applied to threads) and the available cores (Part 10) decide which threads actually advance at any instant — interleaved on one core, or truly in parallel across several.

Why this is useful: two concrete shapes

Threads earn their place in two recognisable situations, both from real programs.

A server handling many requests. A backend server runs as one process. If 100 requests arrive and a single thread handles them one after another, each waits for the previous to finish — slow. Instead the process uses many threads: each request is handled by its own thread, so 100 requests progress together (interleaved and, on multiple cores, partly in parallel) rather than strictly in line. This is the canonical reason server runtimes are multi-threaded.

One program doing several things at once. A music player is one process that must play audio, update the elapsed-time display, respond to clicks, and manage a playlist — all seemingly simultaneously. Different threads handle different jobs: one keeps audio flowing, another updates the clock, another watches for input. Each is a separate path of execution through the same program's code, so the display can update while audio plays, with no job blocking the others.

In both cases the pattern is the same: one process, many threads, each a separate runner through the shared code and data.

The crucial split: shared heap, separate stacks

This is the most important structural fact about threads, and it follows directly from Parts 7 and 8.

Threads of one process share the process's memory — the code, the data segment, and above all the heap. There is one heap for the process, and every thread can reach the same objects on it. This is what makes threads powerful and dangerous: two threads can read and write the same object easily (powerful — cheap communication), but if they do so carelessly at the same time, they can corrupt it (dangerous — the subject of locking, which this path notes but does not dive into).

Diagram
one HEAP, shared:   Thread 1 and Thread 2 can both reach the same object

But each thread has its own stack. This is not optional — it is forced by what a stack is. A stack holds the chain of active call frames for one path of execution (Part 7). Two threads running different methods at the same time each have their own chain of calls in progress; those chains cannot share one stack, because each thread's frames, its stack pointer, and its base pointer are entirely its own. If they shared a stack, one thread's calls would overwrite the other's frames and both would lose their place.

Diagram
Thread 1 -> its own stack of frames   (its own SP, BP, calls in progress)
Thread 2 -> its own stack of frames   (independent chain)

So the rule: one heap per process (shared), one stack per thread (private). Shared heap is why threads communicate easily (unlike isolated processes, Part 6); private stacks are why each can be in a different place in the code at once. This single distinction explains most of how threads behave.

What the shared heap means for a server full of requests

The shared heap raises a fair worry: if a server is one process with one heap, and many customers use it at once, do they all share the same memory — and isn't that a problem? Working through this answer also settles a distinction that matters for every C# design later: instance versus static.

First, requests get threads, not customers. A "customer logging in" is really a request arriving at the server. Each request is handled by a thread (drawn from a pool, Part 13), and when the request finishes, the thread goes back to the pool and serves someone else's next request. A thread is not owned by a customer; the same thread handles thousands of different customers' requests over time, one after another, and a single customer's successive requests may each run on a different thread.

Diagram
Customer A's login request  -> thread 7   (then thread 7 returns to the pool)
Customer B's login request  -> thread 3
Customer A's next request   -> thread 12  (a different thread than before)

Second, a shared heap does not mean shared data. This is the key. The heap is one memory region all threads can reach — but each request, as it runs, creates its own objects with new, and only that request holds references to them (Part 8's reachability). Customer A's request builds A's objects; Customer B's request builds B's objects; both live on the one heap, but they are different objects, and A's code cannot see B's because A has no reference to them.

Diagram
ONE HEAP (shared region)
   [A's user object]   <- only A's request references it   -> private to A
   [A's cart object]   <- only A's request references it   -> private to A
   [B's user object]   <- only B's request references it   -> private to B

Same heap, separate objects, kept apart by references, not by walls. It is one parking lot (the heap) with each person's own car (their objects) — sharing the lot does not mean sharing cars. This is also why each thread does not get its own heap: the privacy requests need already comes from references, while a shared heap is what lets threads hand objects to one another cheaply — the very reason to use threads over isolated processes.

Instance versus static: the distinction to carry everywhere

Underneath that is a split worth anchoring now, because it recurs in every later C# design decision:

  • An instance is an object made with new — the Customer, the request's data. Each new produces a separate object. If every request does its own new, every request gets its own instance, automatically private (only it references that instance). This is the default, and it is why per-request data does not collide.
  • A static member belongs to the type itself, once for the whole process — not to any object. A static field is a single variable shared by every thread and every request. There is exactly one of it.
Diagram
new Customer()  -> a fresh INSTANCE each time   -> one per request (private)
static field    -> ONE value for the whole process -> shared by all requests

The practical question "do I need an instance, or is static fine?" becomes answerable: use an instance when each request (or each use) needs its own separate copy of the data — a user, a cart, a request context. Static is fine when there is genuinely one thing for the whole program and it is not mutated per request — a constant, a stateless helper, configuration read once. The deciding factor is per-request data wants an instance; one-for-everyone data can be static.

Where the shared heap actually bites: shared mutable state

Now the worry becomes precise. Per-request instances are safe because they are not shared. The danger is only when two requests touch the same object at the same time — shared mutable state:

csharp
class Stats
{
    public static int LoginCount;   // ONE variable, shared by every request
 
    public static void RecordLogin()
    {
        LoginCount++;               // many threads hit the SAME variable at once
    }
}

Every customer's login increments the same LoginCount. Two threads can both read 5 and both write 6, losing an increment — a race condition. This is the real hazard of the shared heap, and it applies to any object multiple requests share a reference to, not just statics.

One point that removes a common fear: calling a static method is not the same as sharing data. When many requests call a static method, each call runs on its own thread with its own stack frame — its own parameters and locals (Part 7). Those locals are per-call, on each thread's private stack, so a static method that only uses its own parameters and locals is safe to call from any number of requests at once. It is static state (a static field, or any shared mutable object), not static methods, that needs care.

Diagram
static method's locals/params   -> per call, on each thread's own stack   -> safe
a request's own new'd object    -> only that request references it          -> safe
a static field / shared object  -> one thing, many threads write it         -> race condition

The rule to keep: the question is never "is it static?" but "is there one piece of data that several requests touch at the same time?" If no, it is safe; if yes and it is written concurrently, it needs synchronisation (a lock or a thread-safe type) — the field called thread safety, which this path flags here and treats as its own concern rather than diving in now.

Why thread switching is cheaper than process switching

Part 9 noted switching between processes is expensive. Switching between threads of the same process is cheaper, and now the reason is clear. A process switch must swap the whole process state — including its isolated memory mapping (the virtual-memory address space, Part 6). A thread switch, between two threads of the same process, does not change the memory space at all — both threads already share it. The OS only has to save and restore the smaller per-thread state: the program counter, the stack and base pointers, and the registers.

Diagram
process switch:  save/restore registers  +  switch the whole address space   (expensive)
thread switch:   save/restore registers only; same address space             (cheaper)

Less to swap means a faster switch. This is a core reason a single program does many things with threads rather than many processes: besides the easy shared-memory communication, threads are simply cheaper to interleave.

Who tracks and schedules threads: the kernel

The operating system kernel (Part 6) tracks threads much as it tracks processes — it keeps per-thread execution state and decides which thread runs on which core or logical processor, and when to switch. A useful detail from lower-level notes: a process mostly tracks its own memory (code, data, stack, heap) and its main thread; the kernel is what keeps the full registry of all the threads and schedules them. So threads are scheduled entities the kernel manages, placed onto the hardware execution slots of Part 10.

The .NET angle, and the cost of a thread

In .NET, a thread is a real OS thread, exposed through the System.Threading.Thread type. Everything above holds exactly: a .NET process starts with a main thread, can create more, and all share the process's managed heap (which is why threads can hand objects to each other, and why thread-safety matters).

The cost worth carrying forward: each thread needs its own stack, and that stack reserves real memory — about 1 MB by default for a .NET thread on Windows (the figure is larger, around 8 MB, for a typical native thread on Linux; .NET sets its own default). A thread also carries kernel bookkeeping and a switching cost. So threads are useful but not free — a program cannot simply make tens of thousands of them without consuming a lot of memory and switching overhead.

This cost is the seed of the next three parts. If each thread costs around a megabyte of stack plus scheduling overhead, then creating a fresh thread for every small piece of work — every one of a server's thousands of short requests — is wasteful. That problem is what the thread pool (Part 13) solves by reusing a managed set of threads; and the deeper realisation that a thread waiting on I/O is a thread wasted (Part 14) is what makes async (Part 19) worth having. Threads are the foundation; the parts ahead are about using them economically.

Four confusions worth clearing

These are the exact points where the thread picture tends to tangle. Each has a short, plain answer.

1. The CPU has one program counter — so how does each thread have "its own"?

Both are true, and they fit like this: there is one physical program-counter register per core, and it holds the program counter of whichever thread is running right now. A thread's "own program counter" is a saved value, not a second physical register.

When the OS switches threads, it saves the running thread's program-counter value out to that thread's saved state, and loads the next thread's saved value into the one real register (the same save/restore from Part 9).

Diagram
one physical PC register per core  ->  holds the RUNNING thread's PC value
 
Thread A (running)  ->  its PC value is loaded in the real register
Thread B (paused)   ->  its PC value sits parked in B's saved state
switch to B: save A's PC out, load B's PC in

So "every thread has its own program counter" means every thread owns a saved PC value; only the running one is loaded into the single physical register.

2. Does each thread have its own heap too?

No — and this is the correction to watch for. The rule is one heap per process, shared by all threads; one stack per thread, private. There is no per-thread heap.

Diagram
PROCESS
+-- HEAP   (ONE, shared)   <- objects live here; any thread can reach them
+-- Thread 1 -> its own STACK (private)
+-- Thread 2 -> its own STACK (private)
  • Each thread needs its own stack because a stack is one path's chain of call frames (Part 7). If Thread 1 is inside DoWork -> Add and Thread 2 is inside Handle -> Save at the same moment, those are two separate towers of frames; sharing one stack would make them overwrite each other.
  • The heap is shared on purpose — the whole reason to use threads instead of separate processes is that they can work on the same objects cheaply. A per-thread heap would remove that, which is the point of threads.

3. Is C# single-threaded?

The runtime is not — even a trivial C# program runs with several threads (the main thread plus background threads like the garbage collector). But the code written by default runs on one thread (the main thread) until concurrency is introduced deliberately with a thread, a Task, Parallel, or async.

So: C# the runtime is multi-threaded; a typical program's own code is single-threaded until it is made otherwise.

4. The thread runs the program — but isn't it the CPU with its program counter that runs it?

Both, at two levels — they are not competing claims.

  • The CPU core is the physical engine: it does the fetch-decode-execute loop using its one real program-counter register.
  • A thread is the execution state — a program-counter value, registers, and a stack — that represents one path through the program.

A thread does not run on its own; it runs when the OS loads its state onto a core and lets the core execute it. Pausing the thread saves that state back out (a context switch).

The sentence to keep: the core is what runs; a thread is the state being run. "The thread runs the program" and "the CPU runs the program" describe the same event from two levels — the engine, and the state loaded into it. One core can run many threads over time by swapping their states; many cores can run many threads at once (Part 10).

The one idea to hold onto

A thread is a single path of execution — a program counter, register values, and its own stack of frames — and it is the thing that actually runs a program; the process is only the container. One process starts with a main thread and can create many, all sharing the process's heap, code, and data but each with its own stack. Shared heap makes threads communicate cheaply (and makes thread-safety a concern); private stacks let each thread be in a different place in the code at once. Thread switching is cheaper than process switching because the shared address space does not change. The kernel schedules threads onto cores, and each thread costs real memory (about 1 MB of stack in .NET), which is why the parts ahead focus on using them economically.

What comes next

Threads are defined, and so is their cost. Part 12 looks at creating threads directly in C# with the Thread type and thread state (ready / running / blocked), and then asks the question that cost raises: if each thread is expensive, is making one per task a good idea? The answer is no — which opens Part 13, the thread pool: a reusable set of worker threads the runtime hands out and takes back, so short pieces of work do not each pay the full price of a thread.