Multithreading
Multithreading is a programming technique in which an application uses multiple threads to perform different pieces of work. A thread is an independent path of execution inside a process.
What is a Thread?
A thread is the smallest sequence of programmed instructions that can be managed independently by the operating system and runtime.
When a C# application starts, it has at least one thread called the main thread. Additional threads can be created when an application needs concurrent execution.
Process vs Thread
| Process | Thread |
|---|---|
| An executing application | An execution path inside a process |
| Has its own memory space | Shares process resources |
| Generally heavier | Generally lighter than a process |
| Can contain multiple threads | Belongs to a process |
Single Thread Execution
In a simple program, statements normally execute one after another on the main thread.
using System;
class Program
{
static void Main()
{
Console.WriteLine("Task 1");
Console.WriteLine("Task 2");
Console.WriteLine("Task 3");
}
}
Output
Task 1
Task 2
Task 3
The statements execute sequentially on the same execution path.
Creating a Thread in C#
C# provides the Thread class in the System.Threading namespace for creating and controlling threads.
using System;
using System.Threading;
class Program
{
static void PrintMessage()
{
Console.WriteLine("Running on a separate thread");
}
static void Main()
{
Thread thread = new Thread(PrintMessage);
thread.Start();
Console.WriteLine("Main thread is running");
}
}
The Thread object is created with a method that
contains the work. Calling Start() begins execution
of that thread.
Thread.Start()
The Start() method begins execution of the thread.
Thread thread = new Thread(PrintMessage);
thread.Start();
Creating a Thread object does not start its execution.
Start() is required to begin the thread.
Thread.Sleep()
Thread.Sleep() pauses the current thread for the specified amount of time.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Start");
Thread.Sleep(2000);
Console.WriteLine("End");
}
}
Output
Start
End
The current thread pauses for approximately two seconds before executing the next statement.
Thread.Join()
The Join() method makes the calling thread wait until another thread has completed.
using System;
using System.Threading;
class Program
{
static void Worker()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine("Worker: " + i);
Thread.Sleep(500);
}
}
static void Main()
{
Thread thread = new Thread(Worker);
thread.Start();
thread.Join();
Console.WriteLine("Worker thread completed");
}
}
Output
Worker: 1
Worker: 2
Worker: 3
Worker thread completed
Thread Lifecycle and States
During its lifetime, a thread moves through different execution states. The following diagram shows the basic flow of a thread from creation to completion.
The lifecycle can be understood as a sequence of states. A thread
is created first, started using Start(), scheduled
for execution, and then may temporarily wait before becoming
ready to run again.
Important Thread States
- Unstarted: Thread object is created but Start() has not been called.
- Runnable / Ready: Thread is ready to execute and is waiting for CPU scheduling.
- Running: Thread is currently executing its instructions.
- WaitSleepJoin: Thread is waiting, sleeping, or joining another thread.
- Stopped: Thread has completed execution.
Checking Thread State in C#
using System;
using System.Threading;
class Program
{
static void Work()
{
Thread.Sleep(1000);
}
static void Main()
{
Thread thread = new Thread(Work);
Console.WriteLine(thread.ThreadState);
thread.Start();
Console.WriteLine(thread.ThreadState);
thread.Join();
Console.WriteLine(thread.ThreadState);
}
}
Thread states can change quickly during execution, so the exact state observed at a particular moment depends on timing.
Thread ID
Each thread has a managed thread identifier that can be useful while debugging or tracing concurrent execution.
using System;
using System.Threading;
class Program
{
static void ShowThread()
{
Console.WriteLine(
"Worker Thread ID: " +
Thread.CurrentThread.ManagedThreadId);
}
static void Main()
{
Console.WriteLine(
"Main Thread ID: " +
Thread.CurrentThread.ManagedThreadId);
Thread thread = new Thread(ShowThread);
thread.Start();
thread.Join();
}
}
The actual numeric IDs can be different each time the program runs.
Example: Multiple Threads
An application can create multiple threads, with each thread performing a separate piece of work.
using System;
using System.Threading;
class Program
{
static void TaskOne()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine("Task One: " + i);
Thread.Sleep(300);
}
}
static void TaskTwo()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine("Task Two: " + i);
Thread.Sleep(300);
}
}
static void Main()
{
Thread thread1 = new Thread(TaskOne);
Thread thread2 = new Thread(TaskTwo);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Both tasks completed");
}
}
Possible Output
Task One: 1
Task Two: 1
Task One: 2
Task Two: 2
Task Two: 3
Task One: 3
Task One: 4
Task Two: 4
Task One: 5
Task Two: 5
Both tasks completed
Shared Data and Race Conditions
Multiple threads can access the same data. If two or more threads modify shared data at the same time, the final result may become unpredictable.
This situation is commonly known as a race condition.
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 10000; i++)
{
counter++;
}
}
static void Main()
{
Thread thread1 = new Thread(Increment);
Thread thread2 = new Thread(Increment);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Counter = " + counter);
}
}
It may be tempting to expect exactly 20000, but unsynchronized concurrent updates can produce an unexpected result.
Synchronization Using lock
C# provides the lock statement to protect a critical section so that only one thread at a time can execute that section for the same lock object.
using System;
using System.Threading;
class Program
{
static int counter = 0;
static object lockObject = new object();
static void Increment()
{
for (int i = 0; i < 10000; i++)
{
lock (lockObject)
{
counter++;
}
}
}
static void Main()
{
Thread thread1 = new Thread(Increment);
Thread thread2 = new Thread(Increment);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Counter = " + counter);
}
}
Output
Counter = 20000
The lock ensures that the increment operation inside the critical section is protected from simultaneous access by another thread using the same lock object.
Monitor
The Monitor class provides synchronization mechanisms for controlling access to shared resources.
using System;
using System.Threading;
class Program
{
static object lockObject = new object();
static void PrintMessage()
{
Monitor.Enter(lockObject);
try
{
Console.WriteLine("Thread entered critical section");
}
finally
{
Monitor.Exit(lockObject);
}
}
static void Main()
{
Thread thread = new Thread(PrintMessage);
thread.Start();
thread.Join();
}
}
The lock statement is generally simpler to use,
while Monitor provides more control over synchronization.
Thread Priority
A thread can have a priority that influences scheduling. Available priorities include:
- Lowest
- BelowNormal
- Normal
- AboveNormal
- Highest
Thread thread = new Thread(Work);
thread.Priority = ThreadPriority.AboveNormal;
thread.Start();
Background Thread
A thread can be marked as a background thread using the
IsBackground property.
Thread thread = new Thread(Work);
thread.IsBackground = true;
thread.Start();
Background threads do not keep the application alive after all foreground threads have ended.
Thread Safety
Code is considered thread-safe when it behaves correctly even when accessed concurrently by multiple threads.
Thread safety becomes important when multiple threads share mutable data or access shared resources.
Interlocked
The Interlocked class provides atomic operations for simple shared values such as counters.
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 10000; i++)
{
Interlocked.Increment(ref counter);
}
}
static void Main()
{
Thread thread1 = new Thread(Increment);
Thread thread2 = new Thread(Increment);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Counter = " + counter);
}
}
Output
Counter = 20000
ThreadPool
Creating a new Thread manually for every small piece of work can be expensive. .NET provides the ThreadPool to manage reusable worker threads.
using System;
using System.Threading;
class Program
{
static void Work(object? state)
{
Console.WriteLine("Work executed by ThreadPool");
}
static void Main()
{
ThreadPool.QueueUserWorkItem(Work);
Thread.Sleep(1000);
}
}
The ThreadPool maintains a collection of worker threads that can execute queued work items.
Thread vs ThreadPool
| Thread | ThreadPool |
|---|---|
| Created and managed explicitly | Managed by .NET |
| More control | Convenient for short work items |
| Creation has overhead | Reuses existing worker threads |
| Useful when specific thread control is required | Useful for general background work |
Real-World Uses of Multithreading
Multithreading can be useful in applications that need concurrent execution of independent work.
- Desktop applications
- Background processing
- Logging systems
- File processing
- Data processing workloads
- Server-side applications
- Applications that perform multiple independent operations
Common Multithreading Mistakes
1. Ignoring Shared Data
Multiple threads modifying the same variable can create unpredictable results.
2. Incorrect Locking
Poor synchronization can lead to deadlocks, unnecessary blocking, or performance problems.
3. Creating Too Many Threads
Creating large numbers of threads can consume resources and reduce application performance.
4. Assuming Output Order
When multiple threads run concurrently, their output order should not normally be assumed to be fixed.
Multithreading vs Asynchronous Programming
| Multithreading | Asynchronous Programming |
|---|---|
| Focuses on concurrent execution using threads | Focuses on handling asynchronous operations |
| Threads are an important part of the model | Does not automatically require creating threads |
| Shared state requires synchronization | Often used for I/O-bound operations |
| Useful for concurrent work | Useful for operations that spend time waiting |
Interview Questions
1. What is multithreading?
Multithreading is a technique where multiple threads execute different pieces of work within the same process.
2. What is a thread?
A thread is an independent execution path within a process.
3. How do you create a thread in C#?
A thread can be created using the
System.Threading.Thread class.
4. What is Thread.Start()?
Start() begins execution of the thread.
5. What is Thread.Join()?
Join() causes the calling thread to wait until the specified thread has completed.
6. What is a race condition?
A race condition occurs when concurrent operations access shared data and the result depends on the timing of execution.
7. What is lock in C#?
The lock statement provides mutual exclusion for a critical section protected by the same lock object.
8. What is ThreadPool?
ThreadPool is a .NET-managed collection of reusable worker threads used for executing queued work.
Practice Programs
- Create a thread that prints numbers from 1 to 10.
- Create two threads that print different messages.
- Use Thread.Join() to wait for a worker thread.
- Create a shared counter and observe the race condition.
- Fix the shared counter using the lock statement.
- Implement the same counter using Interlocked.Increment().
- Display the ManagedThreadId of the main and worker threads.
Summary
- A thread is an independent execution path inside a process.
- C# provides the Thread class for explicit thread creation.
- Start() begins thread execution.
- Join() waits for another thread to finish.
- A thread moves through different lifecycle states during its execution.
- Multiple threads can access shared resources.
- Shared mutable data can cause race conditions.
- lock can protect a critical section.
- Interlocked provides atomic operations for certain shared values.
- ThreadPool provides reusable worker threads managed by .NET.
- Multithreading requires careful synchronization and resource management.