AlgoInsight

Free lesson · Concurrency Interview Mastery

Creating Threads in C#

Medium10 min readconcurrencymultithreadingcsharp

C# and the .NET runtime give you a full threading toolkit, but here's the guidance I'd lead with, because it shapes everything: in modern C#, you rarely create raw Thread objects. The language and runtime push you toward higher-level abstractions - Task and the Task Parallel Library, and async/await - that handle thread management, pooling, and result retrieval for you. Raw Thread still exists and you should understand it, but reaching for it in everyday code is usually a sign you're working at the wrong level. This lesson covers the raw mechanics and then makes clear where the higher-level tools take over.

The conceptual model is the same one from Module 1 - threads share memory, races lurk in shared mutable state, the lifecycle moves through familiar states. What's specific to C# is the API surface and one important distinction: foreground versus background threads, which determines whether your program waits for a thread before exiting.

The Thread class

The raw building block is System.Threading.Thread. You give it a delegate (a method or lambda) to run, then Start() it.

using System.Threading;

void Worker() {
    Console.WriteLine(
quot;Running on thread {Thread.CurrentThread.ManagedThreadId}"); } Thread t = new Thread(Worker); t.Start(); // starts a new thread, runs Worker on it t.Join(); // wait for it to finish

Lambdas are the idiomatic way to give inline work, and to pass data you simply capture it:

int value = 42;
Thread t = new Thread(() => {
    Console.WriteLine(
quot;Captured {value}"); }); t.Start(); t.Join();

There's also a ParameterizedThreadStart form that accepts a single object parameter, but lambda capture is cleaner and type-safe, so prefer it.

Foreground vs background threads

This is the C#-specific distinction worth knowing well. Every thread is either foreground or background, and it determines whether the application waits for the thread before exiting.

Thread t = new Thread(Worker);
t.IsBackground = true;   // set BEFORE Start()
t.Start();
flowchart TD
    M[Main/foreground threads running] --> D{All foreground threads done?}
    D -->|No| M
    D -->|Yes| E[Runtime exits]
    E --> A["Background threads abandoned<br/>(killed mid-work, no cleanup)"]
This flowchart shows how foreground and background threads affect application exit in C#. While any foreground threads are still running, the application keeps running. The runtime repeatedly checks whether all foreground threads are done. As long as some remain, it keeps going. Once all foreground threads have finished, the runtime exits - and at that point any still-running background threads are abandoned, killed mid-work with no cleanup. So foreground threads keep the app alive; background threads do not and can be terminated abruptly when the app exits.

Warning: Background threads are the C# equivalent of daemon threads, and they carry the same hazard: they can be killed abruptly when the application exits, mid-operation, with no guarantee that cleanup or finally blocks run. Never put work that must complete - flushing important data, releasing external resources - on a background thread without ensuring the app stays alive until it finishes. Conversely, a forgotten foreground thread that never terminates will prevent your application from exiting, which is its own common bug. Choose deliberately: foreground for must-finish work the app should wait on, background for disposable helpers. Note that thread-pool threads and Task threads are background by default, which matters for the higher-level tools.

start vs the work itself, and join

The familiar mechanics: Start() launches the thread and runs your delegate on it. Calling the delegate directly would just run it synchronously on the current thread, the same start-vs-run trap from every language in this course. Join() blocks the caller until the thread finishes, with an optional timeout overload (Join(milliseconds)). Other essentials: Thread.Sleep(ms) parks the current thread, Thread.Yield() hints the scheduler to run another thread, and Thread.CurrentThread.ManagedThreadId identifies the current thread for logging.

Getting a result back

Like most languages' raw thread APIs, a Thread's delegate return value is discarded - there's no built-in result channel on Thread. To get a result the old way, you'd capture a shared variable and read it after Join(). But this is precisely where C# steers you to the modern tools: Task<T> is designed to return a result cleanly.

// The modern way - Task returns a result directly:
Task<int> task = Task.Run(() => Compute());
int result = await task;        // or task.Result to block

This Task approach (covered fully in the TPL lesson) is the idiomatic modern way to run work and get a result. For "run this and give me the answer," you reach for Task.Run, not new Thread.

The thread pool and why Task wins

Creating a fresh Thread per task is expensive and unbounded - the same problem from every language. .NET provides a managed thread pool to reuse threads, historically accessed via ThreadPool.QueueUserWorkItem. But you rarely call the thread pool directly anymore either, because Task sits on top of it with a far better API.

// Low-level pool (rarely used directly now):
ThreadPool.QueueUserWorkItem(state => DoWork());

// Modern equivalent - same pooling, much better API:
Task.Run(() => DoWork());

Tip: The practical hierarchy in modern C#: use async/await with Task for I/O-bound work, use Task.Run (or the Parallel class) for CPU-bound work you want on the thread pool, and reserve raw Thread for the rare case where you need a dedicated long-lived thread with specific properties - a thread you control directly, set priority on, or need to be foreground. "I'd use Task rather than creating a Thread directly" is the modern, correct instinct, and the TPL lesson shows why Task is so much more capable.

Why modern C# moved away from raw threads

It's worth understanding the trajectory, because it explains why "use Task, not Thread" isn't just a style preference but the direction the entire platform has moved. Early .NET gave you Thread and the raw thread pool, and you managed everything yourself: creating threads, getting results back through shared state, coordinating completion, propagating exceptions, and handling cancellation by hand. This worked but was verbose and error-prone, and crucially it didn't compose - combining the results of several concurrent operations, or chaining one after another, meant writing a lot of careful, bug-prone plumbing.

The Task Parallel Library, introduced to address this, reframed concurrency around the Task - a first-class object representing a unit of asynchronous work that can be composed, awaited, cancelled, and that carries its result and any exception. Then async/await layered a clean syntax on top, letting you write asynchronous code that reads almost like sequential code while the compiler handles the state machine underneath. The combined effect is that the things which were painful with raw threads - getting results, combining concurrent operations, propagating exceptions, cancelling work - became natural and concise. Raw Thread didn't disappear, but it stopped being the tool you reach for first, the same way you wouldn't manage memory by hand when the platform gives you better abstractions.

There's also a resource dimension. Raw threads are heavyweight - each consumes around a megabyte of stack and is scheduled by the OS - so creating one per unit of work doesn't scale. The thread pool, which Task sits on, reuses a managed set of threads across many work items, amortizing creation cost and bounding how many threads exist. When you call Task.Run, you're handing work to that pool rather than minting a fresh OS thread, which is why task-based code scales to far more concurrent operations than thread-per-task code ever could. Understanding this is why the modern guidance is so consistent: tasks give you better ergonomics and better resource behavior, and raw threads are left for the narrow cases where neither matters.

When you genuinely need a raw Thread

Raw Thread still has legitimate uses, and naming them shows you understand the tradeoff rather than just parroting "always use Task." You'd reach for a raw Thread when you need a dedicated, long-lived thread with specific properties that the thread pool doesn't give you. The thread pool is designed for short work items that complete and return the thread for reuse; a thread that runs for the entire lifetime of the application, blocking on some resource, would tie up a pool thread indefinitely and is better as a dedicated Thread.

Specific reasons to create a raw thread include needing to set a custom thread priority, needing a particular stack size, needing the thread to be foreground so it keeps the app alive, or - in older desktop UI frameworks - needing a specific apartment state for COM interop. A classic example is a dedicated background loop that consumes from a queue for the application's whole life: making it a long-running pool task can starve the pool, so a dedicated thread (or a task created with the LongRunning option, which hints the runtime to use a dedicated thread) is more appropriate. These cases are real but uncommon. The decision rule is: short, poolable work items go to Task; dedicated long-lived threads with special properties justify a raw Thread. Reaching for raw threads outside those cases is usually working at the wrong level.

Thread states and lifecycle in C#

The thread lifecycle from Module 1 applies in C# with its own vocabulary, and being able to reason about which state a thread is in is the same debugging superpower here as everywhere. A Thread exposes a ThreadState property reflecting where it is: unstarted before Start(), running or waiting-to-run once started, blocked when waiting on a lock, in a wait/sleep/join state when parked, and stopped when finished. As in Java, these states tell you the story when a program hangs: threads blocked on a lock point to contention or deadlock, threads in a wait state point to signaling, and threads all runnable but the program crawling points to CPU saturation rather than a locking problem.

In practice, with modern C# leaning on tasks and the thread pool, you inspect this less through ThreadState on raw Thread objects and more through debugger tooling and diagnostics that show what pool threads and tasks are doing. But the conceptual states are identical, and the first move when a concurrent C# program misbehaves is the same as in every language: determine what each thread is actually doing - blocked, waiting, running, or finished - before forming a theory about the cause. The state of the world tells you which category of problem you're chasing, and guessing before you've looked is how concurrency debugging goes wrong.

One C#-specific transition worth noting is interruption and abort. C# historically had Thread.Abort, a forcible thread-termination mechanism, but it's deprecated and unsupported in modern .NET for exactly the reasons covered in Module 1: forcibly killing a thread can leave locks held and shared state corrupted, because the thread dies mid-operation without running its cleanup. The supported model is cooperative cancellation via CancellationToken (covered in the TPL lesson) - you signal a thread or task to stop, and it checks the signal at safe points and bows out cleanly. "You don't forcibly kill threads in modern C#; you cancel cooperatively" is both the correct practice and a good interview answer.

Thread safety is your responsibility

A final framing that carries through the rest of the C# section: creating a thread is easy, but everything those threads touch becomes your responsibility to make safe. The moment you have two threads, any shared mutable state between them - a field, a collection, an object's properties - is a potential race, exactly as Module 1 established, and C# does nothing automatic to protect it. A List<T> accessed from multiple threads without synchronization will corrupt or throw; a shared counter incremented from multiple threads loses updates; an object whose fields are read and written concurrently can be seen in an inconsistent half-updated state.

C# gives you the tools to make shared access safe - the lock statement, the Interlocked class, volatile, and thread-safe collections in System.Collections.Concurrent like ConcurrentDictionary and ConcurrentQueue - but it's on you to apply them. The single most reliable way to avoid trouble is to minimize shared mutable state in the first place: prefer passing data into tasks and returning results over sharing variables, prefer immutable objects where you can, and use the concurrent collections rather than locking plain ones by hand. This is the same lesson as every language in this course, stated in C# terms: the danger isn't the threads, it's the shared state, and reducing or properly guarding that state is the whole game. The next several lessons give you the specific tools; this lesson's job is to make sure you understand that the responsibility is yours from the moment you start a second thread.

Note: The throughline for the whole C# section: the language has spent its evolution moving you up the abstraction ladder, from raw Thread to the thread pool to Task to async/await. Each rung handles more for you - pooling, results, exceptions, cancellation, composition - so the modern instinct is to start at the highest rung that fits and only descend when you have a specific reason. This lesson covered the bottom rung so you understand the foundation; the lessons that follow are about the higher rungs you'll actually live on day to day.

The interview angle

Interview: Likely questions: (1) "Thread vs Task in C#?" - Thread is a low-level OS thread you manage directly; Task is a higher-level abstraction over the thread pool that's composable, returns results, supports cancellation and continuations, and is the modern default. Prefer Task. (2) "Foreground vs background threads?" - Foreground threads keep the app alive until they finish; background threads don't and are abandoned when the app exits. new Thread is foreground by default; thread-pool and Task threads are background. (3) "How do you get a result from a thread?" - Not from raw Thread (return discarded); use Task<T> and await or .Result. (4) "Why prefer the thread pool / Task over creating threads?" - Thread reuse avoids creation cost, bounds resource use, and Task adds composition, results, and cancellation. Knowing the foreground/background distinction and defaulting to Task signals current C# fluency.

Quick reference

The essentials for raw threads, with the reminder that you'll reach for the higher-level Task API far more often than this in real code:

using System.Threading;

// raw thread
Thread t = new Thread(() => DoWork());
t.IsBackground = true;     // set before Start(); false (foreground) is default
t.Start();
t.Join();                  // or t.Join(milliseconds)

Thread.Sleep(100);
Thread.CurrentThread.ManagedThreadId;

// modern (preferred): Task over the thread pool, returns a result
Task<int> task = Task.Run(() => Compute());
int r = await task;        // or task.Result (blocks)

// RULE: prefer Task/async-await; raw Thread only for dedicated long-lived threads

Next we go beneath threads to the C# memory model - visibility, reordering, volatile, and the Interlocked class - the rules that decide when one thread actually sees another's writes in .NET.

Back to Concurrency Interview Mastery