📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the difference between a process and a thread? How do they map to .NET concepts such as Process, Thread, the thread pool and Task?

What a process owns (address space, handles, security context) versus what a thread owns (stack, registers, thread-local storage), how threads share memory, how .NET threads map to OS threads, how the thread pool and Task build on them, and when to use multiple processes instead of threads.

Junior Theory Beginner concurrency operating-system process tasks thread thread-pool

🎯 Key points

  • A process is a running program with its own virtual address space, handles and security context
  • A thread is a unit of execution inside a process with its own stack, registers and thread-local storage
  • Threads in a process share memory, so they communicate cheaply but need synchronisation
  • Processes are isolated: a crash or memory corruption in one doesn't directly affect another
  • A .NET Thread maps one-to-one to an OS thread; the thread pool reuses a set of threads
  • A Task is a unit of work, not a thread; async I/O tasks often use no thread while waiting
  • Use separate processes (or containers) for isolation and independent scaling; threads for parallelism within one service

⏱ 30-second answer

A process is a running program with its own virtual address space, handles (open files, sockets) and security context; processes are isolated from each other. A thread is a path of execution inside a process with its own stack, registers and thread-local storage, while sharing the process's memory with other threads. Sharing makes communication cheap but requires synchronisation. In .NET, a System.Diagnostics.Process represents an OS process, a Thread is one-to-one with an OS thread, the thread pool keeps a reusable set of threads, and a Task is a unit of work scheduled onto them. An await on I/O usually uses no thread at all while it waits.

🗣 Full answer (senior)

What each owns

Process Thread
Memory Own virtual address space Shares the process's address space
Stack One per thread Own stack (~1–1.5 MB reserved)
CPU state n/a Own registers and instruction pointer
Handles and file descriptors Owns them Uses the process's
Security context Own user and token Usually inherits the process's
Scheduling Not scheduled itself The unit the OS schedules
Failure impact Isolated from other processes An unhandled exception on any thread usually terminates the whole process
Creation cost Higher (new address space, loading) Lower, but still microseconds and a stack

How they communicate

  • Threads: shared memory, guarded by lock, SemaphoreSlim, Interlocked, concurrent collections, Channel<T>.
  • Processes: inter-process communication: pipes, sockets, HTTP/gRPC, message queues, shared memory files, databases.

The .NET layering

Task / async-await          units of work, continuations
   |
ThreadPool                  reusable worker threads + I/O completion handling
   |
System.Threading.Thread     one managed thread = one OS thread
   |
OS thread                   scheduled by the kernel on a core
   |
Process                     dotnet.exe / your apphost, one CLR instance
  • new Thread(...) creates a dedicated OS thread. Use it rarely (long-running loops with special needs).
  • ThreadPool and Task.Run reuse threads, avoiding creation costs. ASP.NET Core requests run on thread-pool threads.
  • async/await on I/O releases the thread while the operation is in progress; the continuation runs later on a pool thread. That's why one server can handle thousands of concurrent requests with a few dozen threads.
  • Task.Factory.StartNew(..., TaskCreationOptions.LongRunning) hints that a dedicated thread is appropriate.
  • One process hosts one runtime instance. AppDomains for isolation are gone in modern .NET; isolation means separate processes (or AssemblyLoadContext for loading and unloading plug-ins, which isn't a security boundary).

When to use processes versus threads

Use threads (within one process) Use separate processes or containers
Parallel CPU work on shared data Fault isolation: a crash shouldn't take everything down
Concurrent I/O in one service Different scaling needs (API versus background worker)
Low-latency communication Different security contexts or least-privilege boundaries
Untrusted plug-ins or native code that might corrupt memory

Production example

A reporting service ran heavy PDF generation on the same process as its API. A native PDF library bug occasionally crashed the process, taking down all API requests too. Moving rendering into a separate worker process fed by a queue isolated the crashes: the worker restarted and retried, and the API kept serving.

🔍 What the interviewer is checking

A fundamentals question that reveals whether the candidate understands concurrency in .NET.

Green flags: lists what each owns, explains sharing and isolation, maps Thread, thread pool and Task correctly, knows async I/O doesn't hold a thread, gives good reasons for separate processes.

Red flags: "a thread is a lightweight process" with nothing more; equates tasks with threads; suggests creating threads per request.

📚 Learn in depth

1. Picture a process with three threads

Process 4312 (MyApi)
+---------------------------------------------------------------+
| virtual address space                                         |
|   code (JIT + native) | GC heap | native heap | mapped files   |
|                                                               |
|   thread 1 stack | thread 2 stack | thread 3 stack           |
|                                                               |
| handle table: files, sockets, events, threads                 |
| security token / uid                                          |
+---------------------------------------------------------------+
   thread 1: registers, IP  thread 2: registers, IP  thread 3: ...
   all three see the same GC heap and statics

2. Linux and Windows details

  • Linux implements threads as tasks that share an address space (created with clone). Tools like top -H and ps -L show threads.
  • Windows has process and thread objects in the kernel; Task Manager and Process Explorer show threads per process.
  • .NET threads have a managed ID (Environment.CurrentManagedThreadId) and an OS thread ID; debuggers and dotnet-dump show both.

3. The thread pool in brief

  • Starts with a minimum number of workers (by default, the processor count) and injects more when work is queued and threads are blocked, using a hill-climbing algorithm (roughly one or two new threads per second beyond the minimum).
  • Blocking calls on pool threads (.Result, .Wait(), synchronous I/O) cause thread pool starvation: requests queue while the pool slowly grows.
  • ThreadPool.GetAvailableThreads, dotnet-counters (threadpool-thread-count, threadpool-queue-length) show the state.

4. Tasks are not threads

await httpClient.GetAsync(url)
   1. start the request (a few microseconds on a pool thread)
   2. return to the caller; the pool thread is free
   3. ... network round trip: NO thread is waiting ...
   4. the OS signals completion (IOCP / epoll); a pool thread runs the continuation

Task.Run(() => CpuWork()) does use a pool thread for the duration of the work. Know which kind of task you have.

5. Process-level concerns in production

Concern Process-level tool
Isolation and restarts Kubernetes pods, Windows services with recovery, App Service instances
Resource limits cgroups (CPU, memory), Windows job objects
Health Liveness and readiness probes
Graceful shutdown IHostApplicationLifetime, handling SIGTERM within the termination grace period

6. Try it yourself

  1. Write a console app that starts three Threads printing their managed and OS thread IDs (Environment.CurrentManagedThreadId and, on Linux, /proc/thread-self or top -H), then compare with three Task.Run calls.
  2. Await 1,000 concurrent Task.Delay(1000) calls and watch the thread count in dotnet-counters: it barely moves.
  3. Replace the delays with Thread.Sleep(1000) inside Task.Run and watch the thread pool grow slowly, then explain why.
  4. Start a child process with Process.Start and communicate with it over standard input and output.

💻 Code

namespace ProcessesAndThreads;

using System.Diagnostics;

public static class Program
{
    public static async Task Main()
    {
        using var current = Process.GetCurrentProcess();
        Console.WriteLine($"Process {current.Id} has {current.Threads.Count} OS threads");

        // Dedicated OS thread (rarely needed)
        var worker = new Thread(() => Console.WriteLine($"Dedicated thread {Environment.CurrentManagedThreadId}"))
        {
            IsBackground = true,
            Name = "dedicated-worker",
        };
        worker.Start();
        worker.Join();

        // Thread pool work (CPU-bound)
        int result = await Task.Run(() => Enumerable.Range(0, 1_000_000).Sum(i => i % 7));
        Console.WriteLine($"CPU work result {result}");

        // 1,000 concurrent waits use almost no threads
        var sw = Stopwatch.StartNew();
        await Task.WhenAll(Enumerable.Range(0, 1_000).Select(_ => Task.Delay(1_000)));
        current.Refresh();
        Console.WriteLine($"1,000 delays in {sw.ElapsedMilliseconds} ms with {current.Threads.Count} OS threads");

        // A separate process for isolation
        var info = new ProcessStartInfo("dotnet", "--version") { RedirectStandardOutput = true };
        using var child = Process.Start(info)!;
        string version = await child.StandardOutput.ReadToEndAsync();
        await child.WaitForExitAsync();
        Console.WriteLine($"Child process {child.Id} reported SDK {version.Trim()}");
    }
}

⚠ Common mistakes

  • Mistake: saying a Task is a thread. Many tasks (async I/O, timers) use no thread while waiting, and many tasks share a few pool threads. Instead: describe a task as a unit of work or a promise of a result, scheduled onto threads when there's code to run.

  • Mistake: creating a new Thread for each piece of work. Thread creation costs time and a stack each, and thousands of threads cause heavy context switching and memory use. Instead: use the thread pool, Task.Run for CPU work, and async I/O.

  • Mistake: blocking pool threads with .Result or .Wait(). Blocked threads can't serve other work, the pool grows slowly, and requests queue up (thread pool starvation). Instead: use async all the way, and bound genuinely blocking work with dedicated threads or a limited scheduler.

  • Mistake: assuming threads are isolated from each other. They share memory, so unsynchronised access corrupts state, and an unhandled exception on any thread usually ends the whole process. Instead: synchronise shared state, prefer immutable data and message passing, and handle exceptions in background work.

  • Mistake: running risky native work in the main service process. A native crash or memory corruption takes the whole service down. Instead: isolate risky or heavy work in a separate process or container with a queue between them.

🏛 Senior insight: trade-offs

Seniors think about processes as the unit of isolation, deployment and scaling, and threads as an implementation detail of concurrency inside a process. In modern .NET services, that means almost never creating threads directly: async I/O for concurrency, the thread pool for CPU work, bounded concurrency with SemaphoreSlim or Channel<T>, and separate processes or containers when failure isolation or independent scaling matters. They watch thread count and pool queue length in production because blocking code is the most common way to break this model.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says switching between threads is cheaper than between processes Why: Checks they can explain what a context switch saves and restores
    How to answer

    A context switch is the OS stopping one thread and starting another on a core: it saves the registers, instruction pointer and stack pointer of the old thread and restores those of the new one. Switching between processes also swaps the address space, which is more expensive. The direct cost is a few microseconds, but the indirect cost is often bigger: the new thread finds caches and TLBs cold. Switches are voluntary when a thread blocks, and involuntary when its time slice ends. Too many threads in a .NET service, usually caused by blocking code that makes the thread pool inject more threads, means more switching, more cache misses, more memory for stacks and more lock contention, so throughput falls and latency rises. The fix is async I/O, bounded parallelism around the core count, and less lock contention.

  2. When: Candidate mentions the OS decides which thread runs Why: Probes scheduling and CPU limits in containers
    How to answer

    The scheduler keeps a queue of runnable threads per core and gives each a time slice. Scheduling is preemptive: when the slice ends, or a higher-priority thread becomes ready, the running thread is interrupted and another runs. Threads that wait for I/O, locks or timers are blocked and don't use CPU. Windows uses priority-based round-robin with temporary boosts; Linux uses a fair scheduler (CFS, and EEVDF from kernel 6.6). For a .NET service the practical points are: don't play with thread priorities; blocking threads wastes them; and in containers, a CPU limit is enforced as a quota per 100 ms period, so a burst of work can use the quota early and get throttled, causing latency spikes. Environment.ProcessorCount follows the limit and sizes the thread pool and GC.

  3. When: Candidate mentions threads sharing memory and locks Why: Connects threads to real deadlock debugging in Visual Studio
    How to answer

    When it hangs, I press Break All so every thread stops where it is. In the Threads window I look for threads sitting in Monitor.Enter, Lock.Enter or Task.Wait, and switch to each one to read its call stack. Parallel Stacks shows all thread stacks merged into one graph, so two threads each holding a lock and waiting for the other's lock stand out immediately. That's the classic opposite-order deadlock. In production I can't attach, so I take a dump with dotnet-dump and run clrstack -all and syncblk. The fix is a consistent lock order, smaller locks, or removing sync-over-async.

↩ Usually asked after

More in Operating System

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me