A thread is the smallest unit of execution inside a process. Every .NET program starts with one thread — the main thread — and you can create additional threads to run work concurrently on multiple CPU cores.
Use raw threads when you need precise control over priority, naming, or lifetime and when none of the higher-level abstractions (Task, Parallel, async/await) are a good fit. For most modern code, prefer Task-based APIs instead.
Project: Basic-Syntax
Thread thread1 = new Thread(WriteThreadId);
Thread thread2 = new Thread(WriteThreadId);
thread1.Name = "Thread 1";
thread2.Name = "Thread 2";
Thread.CurrentThread.Name = "Main Thread";
thread1.Priority = ThreadPriority.Highest;
thread2.Priority = ThreadPriority.Lowest;
thread1.Start();
thread2.Start();| Concept | Detail |
|---|---|
new Thread(method) |
Creates a thread; the thread is not running yet |
.Start() |
Schedules the thread on the OS; it may not execute immediately |
.Name |
Optional label — shows up in debuggers and logs |
Thread.CurrentThread |
Returns the calling thread's object |
ThreadPriority |
Hint to the OS scheduler (Lowest → Highest). The OS is free to ignore it. |
- You need to name a thread for debugging.
- You want to set a custom priority (rare in practice).
- You are learning how the threading model works before using higher-level APIs.
Project: States-of-a-thread
A thread moves through well-defined states during its lifetime:
Unstarted → Running → WaitSleepJoin / Background → Stopped
Console.WriteLine(thread.ThreadState); // "Unstarted"
thread.Start();
Console.WriteLine(thread.ThreadState); // "Running"
Thread.Sleep(3000); // During sleep: "WaitSleepJoin"
// After work(): "Stopped"| State | Meaning |
|---|---|
Unstarted |
Thread created, Start() not called yet |
Running |
Actively executing |
WaitSleepJoin |
Blocked by Sleep, Join, or a wait handle |
Background |
Marked as background — process can exit even if it is still running |
Stopped |
Method returned; thread is done |
- Monitoring or logging thread health.
- Debugging unexpected thread behaviour.
Project: CancelingThread
.NET threads cannot be forcibly aborted safely (Thread.Abort was removed in .NET 5+). Instead, use a shared flag that the worker checks periodically.
bool cancelThread = false;
Thread thread = new Thread(Work);
thread.Start();
var input = Console.ReadLine();
if (input == "c") cancelThread = true;
thread.Join(); // Wait for the thread to finish cleanly
void Work()
{
for (int i = 0; i < 100000; i++)
{
if (cancelThread)
{
Console.WriteLine($"Cancelled at iteration {i}");
break;
}
Thread.SpinWait(300000);
}
}Forcible termination leaves shared state in an inconsistent condition (open files, half-written data, unreleased locks). Cooperative cancellation lets the worker finish its current unit of work and clean up before stopping.
- Long-running background workers that should respond to user input.
- For
Task-based code, useCancellationTokeninstead (seetask-based-asynchronous-programming.md).
Project: Thread-Exception-Handing
Exceptions thrown on a worker thread do not propagate to the thread that started it. You must catch them inside the worker and relay them manually.
List<Exception> exceptions = new List<Exception>();
object lockExceptions = new object();
Thread thread1 = new Thread(Work);
Thread thread2 = new Thread(Work);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
foreach (var ex in exceptions)
Console.WriteLine(ex.Message);
void Work()
{
try
{
throw new InvalidOperationException("An error occurred.");
}
catch (Exception ex)
{
lock (lockExceptions) // List<T> is not thread-safe
{
exceptions.Add(ex);
}
}
}- Wrap all worker logic in
try/catch. - Collect exceptions in a thread-safe way (a
lock-protected list works;ConcurrentBagalso works). - Process exceptions after
Join()returns, back on the calling thread.
- Any multi-threaded code where a worker could fail and the caller needs to know.
- For
Task-based code, exceptions are captured automatically inAggregateException.
Project: DivideAndConquer
Split a large data set into equal segments, sum each segment on its own thread, and combine the partial results after all threads finish.
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0;
int numofThreads = 4;
int segmentLength = array.Length / numofThreads;
Thread[] threads = new Thread[numofThreads];
threads[0] = new Thread(() => { sum1 = SumSegment(0, segmentLength); });
// ... create the other 3 threads similarly
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();
Console.WriteLine(sum1 + sum2 + sum3 + sum4);Each thread works on its own segment — no shared mutable state during computation, so no synchronization is needed. The combine step runs only after all threads have finished (Join).
- CPU-bound work on large arrays or data sets.
- When you want to benchmark single-threaded vs. multi-threaded execution time.
- Use
Task<T>andReturningResultsFromTaskfor cleaner code in production.
Project: thread-local-storage
Each parallel iteration runs on a thread-pool thread. Instead of acquiring a shared lock on every iteration (expensive), give each thread its own local accumulator and only merge once at the end.
int sum = 0;
object lockSum = new object();
Parallel.For(
0,
array.Length,
() => 0, // thread-local initial value
(i, state, tls) =>
{
tls += array[i]; // accumulate locally — no lock needed
return tls;
},
tls =>
{
lock (lockSum) // merge once per thread at the end
{
sum += tls;
}
}
);- Aggregation inside
Parallel.For/Parallel.ForEachwhere locking every iteration would create a bottleneck. - The pattern pays off when the number of iterations is much larger than the number of threads.
Project: Web-Server-Simulation
A minimal pattern that underpins real web servers: a main thread enqueues requests while a dedicated monitoring thread dequeues them and spawns a worker thread per request.
Queue<string?> requestQueue = new Queue<string?>();
Thread monitoringThread = new Thread(MonitorQueue);
monitoringThread.Start();
while (true) // main thread — accepts input
{
var input = Console.ReadLine();
if (input?.ToLower() == "exit") break;
requestQueue.Enqueue(input);
}
void MonitorQueue() // runs on a dedicated thread
{
while (true)
{
if (requestQueue.Count > 0)
{
var input = requestQueue.Dequeue();
new Thread(() => ProcessInput(input)).Start();
}
Thread.Sleep(100);
}
}Note: The plain
Queue<T>used here is not thread-safe. SeeconcurrentQueue.mdfor the production-ready version usingConcurrentQueue<T>.
- As a teaching model for the producer-consumer pattern.
- Real applications should use
ConcurrentQueue<T>,BlockingCollection<T>, orChannel<T>.
Thread |
Task |
|
|---|---|---|
| Abstraction level | Low | High |
| Thread pool | No (always creates a new OS thread) | Yes (Task.Run uses the pool) |
| Return value | Not directly | Task<T> |
| Cancellation | Manual bool flag |
CancellationToken |
| Exception propagation | Manual collection | AggregateException |
| Best for | Custom priority/name, background daemon threads | General async/parallel work |