C# Threading
Running multiple operations simultaneously
🧵 What is Threading?
Threading allows multiple operations to run concurrently within a single application. Threads enable parallel execution, improving performance for CPU-intensive tasks and maintaining responsiveness during long-running operations in your applications.
// Simple thread example
Thread thread = new Thread(() =>
{
Console.WriteLine("Running in separate thread!");
});
thread.Start();
Output:
Running in separate thread!
Key Threading Concepts
Thread Class
Create and manage threads
Thread t = new Thread(Method);
Task Class
Modern threading with Task
Task.Run(() => Method());
Thread Safety
Protect shared resources
lock(obj) { /* code */ }
Synchronization
Coordinate thread execution
thread.Join();
🔹 Creating a Basic Thread
The Thread class in C# enables explicit creation and management of concurrent execution paths. You instantiate a new thread by passing a ThreadStart delegate—typically a method or lambda expression—to the constructor. Calling Start() initiates the thread's execution on a separate core or time-sliced CPU thread. This allows long-running tasks, such as data processing or I/O operations, to run independently without blocking the main application thread. While powerful, manual thread management requires careful handling of resource sharing, synchronization, and lifecycle to avoid common pitfalls like race conditions and deadlocks.
using System;
using System.Threading;
class Program
{
static void PrintNumbers()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"Thread: {i}");
Thread.Sleep(500); // Pause for 500ms
}
}
static void Main()
{
Thread thread = new Thread(PrintNumbers);
thread.Start(); // Start the thread
// Main thread continues
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"Main: {i}");
Thread.Sleep(500);
}
}
}
Output:
Thread: 1
Main: 1
Thread: 2
Main: 2
...
🔹 Using Task for Threading
The Task Parallel Library (TPL) introduces the Task class as a modern, high-level abstraction for asynchronous and parallel operations. Task.Run() efficiently queues work to the .NET thread pool, optimizing resource usage across available CPU cores. Tasks integrate seamlessly with the async/await pattern, simplifying asynchronous programming. They also provide superior exception handling, cancellation support, and continuations compared to raw threads. For most concurrent scenarios—such as parallel computations, I/O-bound operations, or responsive UI development—using Task is recommended over direct Thread manipulation for better performance and maintainability.
using System;
using System.Threading.Tasks;
class Program
{
static void DoWork(string name)
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"{name}: {i}");
Task.Delay(500).Wait();
}
}
static void Main()
{
// Create and start tasks
Task task1 = Task.Run(() => DoWork("Task 1"));
Task task2 = Task.Run(() => DoWork("Task 2"));
// Wait for both tasks to complete
Task.WaitAll(task1, task2);
Console.WriteLine("All tasks completed!");
}
}
Output:
Task 1: 1
Task 2: 1
Task 1: 2
Task 2: 2
...
All tasks completed!
🔹 Thread Synchronization with Join
The Join() method is a fundamental synchronization mechanism that blocks the calling thread until another thread completes its execution. This ensures orderly completion of worker threads before the main thread proceeds, preventing premature application termination. It is essential for scenarios where results from a thread are required sequentially or for coordinating multiple threads. For example, a main thread can spawn several worker threads, call Join() on each, and safely aggregate their outputs. While effective, overuse of Join() can lead to performance bottlenecks; alternative patterns like Task.WaitAll() or async/await may offer better scalability.
using System;
using System.Threading;
class Program
{
static void CountDown()
{
for (int i = 5; i >= 1; i--)
{
Console.WriteLine($"Countdown: {i}");
Thread.Sleep(1000);
}
Console.WriteLine("Blast off!");
}
static void Main()
{
Thread thread = new Thread(CountDown);
thread.Start();
Console.WriteLine("Waiting for countdown...");
thread.Join(); // Wait for thread to finish
Console.WriteLine("Main thread continues after countdown");
}
}
Output:
Waiting for countdown...
Countdown: 5
Countdown: 4
...
Blast off!
Main thread continues after countdown
🔹 Thread Safety with Lock
The lock statement in C# ensures thread safety by enforcing mutual exclusion around critical sections of code that access shared resources. When multiple threads contend for the same data, a lock guarantees that only one thread can execute the protected block at any time, preventing race conditions and data corruption. The lock object (typically a private, static reference type) acts as a synchronization primitive. It is vital to keep locked sections as short as possible to minimize contention and maintain application throughput. For more complex scenarios, consider using higher-level constructs from the System.Threading namespace, such as Monitor, SemaphoreSlim, or ConcurrentCollections.
using System;
using System.Threading;
class Program
{
static int counter = 0;
static object lockObject = new object();
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
lock (lockObject) // Thread-safe increment
{
counter++;
}
}
}
static void Main()
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Final counter: {counter}"); // Should be 2000
}
}
Output:
Final counter: 2000
🔹 Parallel.For for Simple Parallelism
Parallel.For is a powerful construct from the Parallel class that simplifies data parallelism by automatically distributing loop iterations across multiple threads. It intelligently partitions the input range and schedules work on the thread pool, optimizing for available processor cores. This is ideal for CPU-intensive, independent operations like matrix calculations, image processing, or batch data transformations. The framework handles low-level details like thread creation, load balancing, and exception aggregation. However, care must be taken with shared variables to avoid thread-safety issues; using thread-local storage or synchronization primitives within the loop body is often necessary for correct results.
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Console.WriteLine("Sequential loop:");
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Processing {i}");
}
Console.WriteLine("\nParallel loop:");
Parallel.For(0, 5, i =>
{
Console.WriteLine($"Processing {i} on thread {Task.CurrentId}");
});
Console.WriteLine("\nDone!");
}
}
Output:
Sequential loop:
Processing 0
Processing 1...
Parallel loop:
Processing 2 on thread 3
Processing 0 on thread 1...