1. Introduction
If you imagine a process as a supermarket, threads (thread) are the cashiers at different tills, serving customers at the same time. All cashiers work in the same store, but each does its own job in parallel, which makes things faster and more efficient. Threads inside a process let you run multiple tasks at once, sharing common resources and coordinating work inside a single application. The main advantage is that threads can really run in parallel if the CPU supports multitasking.
Practical need for threads
Why would we run multiple threads at all? Here are a few real-life situations:
- You're writing a GUI app and don't want it to "freeze" during a long operation.
- You need to download several files at the same time.
- In a game, enemies should think independently of each other.
Surprisingly, many programs still suffer "freezes" because of inexperienced multithreading. Today we'll learn how to avoid those pitfalls.
The Thread class: the foundation of manual multithreading
The Thread class is a dinosaur in .NET multithreading. Despite newer tools (Task, async/await), working with Thread still makes sense, especially if you want to feel like a thread master "from scratch".
Thread creation pattern
- Create an instance of Thread, passing the method that will run in the thread.
- Start the thread with Start().
- (Optional) Observe what's happening — everything might run concurrently!
2. Starting a thread with Thread
Let's actually feel the magic of parallelism. Add a small class to our hypothetical app that counts to a certain number and prints progress. We'll learn to run that work in a separate thread.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Main thread started!");
// Create a thread object, pointing to the method to execute
Thread workerThread = new Thread(CountToTen);
// Start the new thread
workerThread.Start();
// The main thread is doing something too: printing dots...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // Delay for clarity
}
Console.WriteLine("\nMain thread finished!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Thread] Counting: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[Thread] Done!");
}
}
What happens?
You'll see in the console that dots and "Counting: X" appear interleaved. That's the first sign of multithreading! The main thread prints its dots, and the new thread counts to 10. They don't block each other, like two musicians in a band: one plays drums, the other plays piano. Each contributes, and together it's music.
3. How to pass data to a thread?
Sometimes a thread needs to know not only what to do but also what to work on. If the method for the thread takes parameters, how do you pass them?
Option 1: Using a lambda (anonymous method)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[Thread] {i} / {n}");
Thread.Sleep(300);
}
}
Here we wrap the call to the method we want in a lambda to pass parameters. This is common, since Thread expects a parameterless method (ThreadStart).
Option 2: Use ParameterizedThreadStart
You can use the special delegate ParameterizedThreadStart, which takes a single object parameter.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[Thread] {i} / {max}");
Thread.Sleep(200);
}
}
Yes, the parameter type is object, so you'll need a cast. Not great, but it works! Modern C# prefers the lambda approach.
4. Managing a thread's life
Let's go over useful things the Thread class provides.
| Property / Method | Purpose |
|---|---|
|
Starts the thread (the method provided when creating it) |
|
Waits for the thread to finish (blocks the caller until the other thread ends) |
|
Shows whether the thread is running right now (true/false) |
|
Allows naming a thread (useful for debugging) |
|
Gets the object representing the current thread |
|
Pauses the current thread for ms milliseconds |
Example: Waiting for a thread to finish
Sometimes the main thread should wait for a worker thread to finish.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[Second thread] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[Main thread] Waiting for the second thread to finish...");
t.Join(); // Main thread waits here
Console.WriteLine("[Main thread] Second thread has finished!");
Without Join() the program could exit while the thread is still running. With Join() the main thread patiently waits until all work is done.
5. Useful nuances
Naming threads: so you don't get lost
For debugging you can name threads:
Thread t = new Thread(() =>
{
Console.WriteLine($"This runs in thread: {Thread.CurrentThread.Name}");
});
t.Name = "Counter-Thread";
t.Start();
This helps when there are many threads and each does its own job.
Limits and the real future
Straight away: in modern apps manual thread control via Thread is rare. In practice you often use more powerful and smarter tools (Task, async/await), which we'll cover later. But understanding thread basics is important for:
- Understanding the internals of C# and .NET.
- Job interviews (you may be asked to explain the difference between Thread and Task).
- Diagnosing and fixing tricky issues in large, legacy apps.
Summary diagram: thread lifecycle
stateDiagram-v2
[*] --> New: Thread created
New --> Running: Start()
Running --> Stopped: Method finished
Stopped --> [*]
Now you can create and start threads in C# yourself. You're no longer just a passenger on the train, you're the engineer controlling several cars at once! Ahead: thread lifecycle, management, synchronization, and new horizons of parallelism.
6. Common mistakes and tricks when working with Thread
Mistake #1: not starting the thread.
Often people create a Thread object but forget to call Start(). As a result the thread never runs, and it's not obvious why.
Mistake #2: changing shared data without synchronization.
If multiple threads work with the same variable without protection, expect trouble! It's like two cashiers giving change from the same box — confusion and errors will appear fast.
Mistake #3: using obsolete and unsafe methods.
Don't use methods like Thread.Suspend(), Thread.Resume() and similar — they're unsafe and obsolete. Manage thread lifecycles in other ways.
Mistake #4: unhandled exceptions inside threads.
If an exception occurs in a thread and isn't caught, the thread will terminate and the main thread might not know about it! Wrap thread code in a try-catch to catch errors and log them.
Thread t = new Thread(() =>
{
try
{
// ... your code
}
catch (Exception ex)
{
// Log the error
Console.WriteLine($"[Thread] Error: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION