Thread Basics
Vaibhav • September 11, 2025
In the previous article, we introduced asynchronous programming and saw how it helps applications stay responsive by allowing tasks to run concurrently. At the heart of this capability lies the concept of threads. Understanding threads is essential before diving deeper into async/await, task parallelism, and synchronization. In this article, we’ll explore what threads are, how they work in C#, and how the operating system manages them. We’ll also look at practical examples and common pitfalls to avoid.
What Is a Thread?
A thread is the smallest unit of execution in a program. Every C# application starts with a single thread-the
main thread. This thread runs your Main()
method and handles all
operations unless you explicitly create more threads.
Think of a thread as a worker assigned to do a task. If you only have one worker, they must finish one task before starting another. But if you have multiple workers (threads), they can work on different tasks simultaneously. This is especially useful when some tasks involve waiting-like reading a file or downloading data.
Threads are managed by the operating system. Each thread has its own call stack and CPU context, but shares memory with other threads in the same process.
Creating Threads in C#
You can create a thread using the System.Threading.Thread
class. Here’s a
simple example:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = new Thread(PrintMessage);
t.Start();
Console.WriteLine("Main thread continues...");
}
static void PrintMessage()
{
Console.WriteLine("Hello from another thread!");
}
}
In this example, we define a method PrintMessage
and start a new thread to run
it. The main thread continues executing without waiting for the new thread to finish.
This is the essence of concurrency-multiple threads doing work independently. However, manually managing threads can be tricky and inefficient, especially when scaling to many tasks.
Thread Lifecycle
A thread goes through several states:
- Unstarted: The thread is created but not yet started.
- Running: The thread is executing code.
- WaitSleepJoin: The thread is paused (e.g., sleeping or waiting).
- Stopped: The thread has finished execution.
- Aborted: The thread was forcibly terminated (not recommended).
You can inspect a thread’s state using the Thread.ThreadState
property. But in
most modern C# code, you’ll rarely need to manage thread states manually.
Thread.Sleep and Blocking
Sometimes you want a thread to pause for a while. You can use Thread.Sleep
:
Console.WriteLine("Sleeping for 2 seconds...");
Thread.Sleep(2000);
Console.WriteLine("Awake now!");
This blocks the current thread for 2 seconds. While useful for testing or delaying execution, blocking threads is generally discouraged in production code-especially in UI applications where it freezes the interface.
Note: Blocking a thread means it cannot do anything else during that time. In asynchronous programming, we prefer non-blocking operations that allow the thread to continue working.
Thread Safety and Shared Data
Threads in the same process share memory. This means they can access the same variables and objects. While powerful, this introduces the risk of race conditions-where two threads try to read or write the same data at the same time.
int counter = 0;
void Increment()
{
for (int i = 0; i < 1000; i++)
{
counter++;
}
}
If two threads run Increment()
simultaneously, the final value of counter
may not be 2000. This is because counter++
is not atomic-it involves reading, incrementing, and writing back.
Use synchronization primitives like lock
, Monitor
, or Interlocked
to protect shared
data.
Using lock for Synchronization
The lock
statement ensures that only one thread can enter a critical section at
a time:
object sync = new object();
int counter = 0;
void SafeIncrement()
{
for (int i = 0; i < 1000; i++)
{
lock (sync)
{
counter++;
}
}
}
Here, the lock
ensures that only one thread modifies counter
at a time. This prevents race conditions and ensures data integrity.
Thread Pool and Efficiency
Creating threads manually is expensive. Each thread consumes memory and CPU resources. To solve this, .NET provides a thread pool-a collection of reusable threads managed by the runtime.
Instead of creating new threads, you can queue work to the thread pool using ThreadPool.QueueUserWorkItem
:
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine("Running in thread pool");
});
Console.WriteLine("Main thread continues...");
This is more efficient and scalable. The thread pool automatically manages the number of threads and reuses them for multiple tasks.
Foreground vs Background Threads
Threads can be foreground or background. The difference is subtle but important:
- Foreground threads keep the application alive until they finish.
- Background threads do not prevent the application from exiting.
Thread t = new Thread(() =>
{
Thread.Sleep(5000);
Console.WriteLine("Finished");
});
t.IsBackground = true;
t.Start();
Console.WriteLine("Main thread ends");
In this example, the background thread may not finish if the main thread exits first. Use background threads for tasks that don’t need to complete before shutdown (e.g., logging).
Thread Priorities
You can assign priorities to threads using the Thread.Priority
property. Values
include Lowest
, BelowNormal
, Normal
, AboveNormal
, and Highest
.
Thread t = new Thread(() => Console.WriteLine("Running"));
t.Priority = ThreadPriority.Highest;
t.Start();
However, thread priorities are only hints to the operating system. They don’t guarantee execution order and should be used sparingly.
Common Pitfalls
Working with threads introduces complexity. Here are some common mistakes:
- Accessing shared data without synchronization
- Blocking the UI thread with
Thread.Sleep
or long operations - Creating too many threads manually
- Forgetting to handle exceptions in threads
If a thread throws an exception and it’s not caught, the entire application may crash. Always wrap thread code in try-catch blocks.
Summary
Threads are the foundation of concurrency in C#. They allow your application to perform multiple tasks simultaneously, improving responsiveness and scalability. While powerful, threads must be used carefully to avoid race conditions, deadlocks, and performance issues.
In this article, we explored how threads work, how to create and manage them, and how to synchronize access to shared data. We also introduced the thread pool as a more efficient alternative to manual thread creation.
In the next article, we’ll explore the Task-Based Asynchronous Pattern, which builds on threads and the thread pool to provide a modern, scalable way to write asynchronous code in C#.