Process, Thread, and Multi-threading Concepts
A process represents all computing resources consumed by a running program. A thread is the smallest unit of program execution flow, dependent on processes, where one process can contain multiple threads.
Multi-threading involves multiple execution flows running simultaneously:
- CPU operations utilize time slices through context switching (loading environment → computation → saving environment)
- From a microscopic perspective, a single core executes only one thread at a time
- Macroscopically, multi-threading appears concurrent
- Multiple CPU cores can work independently
Three key characteristics of multi-threading: doesn't block main thread, speed enhancement, and unordered execution.
Synchronous vs Asynchronous Operations
Synchronous: Waits for computation completion before proceeding, causing blocking. Asynchronous: Continues to next line without waiting, non-blocking.
Action<string> operation = this.PerformLengthyOperation;
operation.Invoke("sync_call_1"); // synchronous
operation("sync_call_2"); // synchronous
operation.BeginInvoke("async_call_3", null, null); // asynchronous
Evolution of Threading in Different C# Versions
| C# Version | Technology | Description |
|---|---|---|
| C#1.0 | Thread | Thread waiting, callbacks, foreground/background threads, extensive APIs including Start(), Join(), etc. |
| C#2.0 | ThreadPool | Thread pool management, ManualResetEvent usage, reduced API surface |
| C#3.0 | Task | Enhanced threading model |
Concurrency Programming
Concurrency encompasses multi-threading, parallel processing, asynchronous programming, and reactive programing.
Task Initialization
Why Tasks Are Essential
Task combines Thread and ThreadPool capabilities. While Thread can cause excessive time and space overhead when misused, ThreadPool has limited control over thread continuation, blocking, cancellation, and timeouts. Tasks provide a more sophisticated wrapper around ThreadPool functionality.
private void PerformLengthyOperation(string identifier)
{
Console.WriteLine($"Execution started {identifier} ID {Thread.CurrentThread.ManagedThreadId} {DateTime.Now}");
long calculationResult = 0;
for (int i = 0; i < 1000000000; i++)
{
calculationResult += i;
}
Console.WriteLine($"Execution completed {identifier} ID {Thread.CurrentThread.ManagedThreadId} {DateTime.Now} Result: {calculationResult}");
}
Task Creation Methods
// Method 1
Task.Run(() => this.PerformLengthyOperation("method1"));
// Method 2
Task.Run(() => this.PerformLengthyOperation("method2"));
// Using TaskFactory
TaskFactory factory = Task.Factory;
factory.StartNew(() => this.PerformLengthyOperation("method3"));
// Direct instantiation
new Task(() => this.PerformLengthyOperation("method4")).Start();
Multi-threading Blocking - Task.WaitAll
Scenario: Eating after sleeping
Non-blocking approach (problematic):
List<Task> taskCollection = new List<Task>();
taskCollection.Add(Task.Run(() => SleepProcess("PersonA")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonB")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonC")));
Console.WriteLine("Sleep phase complete, starting meal");
Blocking approach with Task.WaitAll:
List<Task> taskCollection = new List<Task>();
taskCollection.Add(Task.Run(() => SleepProcess("PersonA")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonB")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonC")));
Task.WaitAll(taskCollection.ToArray()); // Blocks until all tasks complete
Console.WriteLine("Sleep phase complete, starting meal");
Non-blocking with nested Tasks:
Task.Run(() =>
{
List<Task> taskCollection = new List<Task>();
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonA")));
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonB")));
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonC")));
Task.WaitAll(taskCollection.ToArray()); // Blocks internally
Console.WriteLine("All awakened");
});
Task.WaitAny Implementation
List<Task> taskCollection = new List<Task>();
taskCollection.Add(Task.Run(() => SleepProcess("PersonA")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonB")));
taskCollection.Add(Task.Run(() => SleepProcess("PersonC")));
Task.WaitAny(taskCollection.ToArray()); // Proceeds when first task completes
Console.WriteLine("First person awake, starting meal");
Non-blocking Multi-threading with WhenAll and WhenAny
List<Task> taskCollection = new List<Task>();
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonA")));
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonB")));
taskCollection.Add(Task.Run(() => PerformLengthyOperation("PersonC")));
Task.WhenAny(taskCollection.ToArray()).ContinueWith(t =>
{
Console.WriteLine("First task completed");
});
Task.WhenAll(taskCollection.ToArray()).ContinueWith(t =>
{
Console.WriteLine("All tasks completed");
});
Task Creation Options
AttachedToParent
Establishes parent-child relationships where parent waits for child completion:
Task parent = new Task(() =>
{
Task child1 = new Task(() =>
{
Thread.Sleep(100);
Debug.WriteLine("child1");
}, TaskCreationOptions.AttachedToParent);
Task child2 = new Task(() =>
{
Thread.Sleep(10);
Debug.WriteLine("child2");
}, TaskCreationOptions.AttachedToParent);
child1.Start();
child2.Start();
});
parent.Start();
parent.Wait();
LongRunning Option
Recommended for long-duration tasks:
if ((task.Options & TaskCreationOptions.LongRunning) != 0)
{
Thread thread = new Thread(longRunningWork);
thread.IsBackground = true;
thread.Start(task);
}
else
{
bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) != TaskCreationOptions.None;
ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal);
}
Task with Return Values
Task<TResult>
Task<int> task1 = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
return 1;
});
Debug.WriteLine(task1.Result);
ContinueWith with Return Values
Task<int> task1 = Task.Factory.StartNew(() =>
{
return 1;
});
var task2 = task1.ContinueWith<string>(t =>
{
int value = t.Result;
var sum = value + 10;
return sum.ToString();
});
Debug.WriteLine(task2.Result);
Thread.Sleep vs Task.Delay
Thread.Sleep blocks current thread:
Stopwatch timer = new Stopwatch();
timer.Start();
Thread.Sleep(2000); // Blocks current thread for 2 seconds
timer.Stop();
Console.WriteLine(timer.ElapsedMilliseconds);
Task.Delay non-blocking approach:
Task.Delay(2000).ContinueWith(t =>
{
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
});
Exception Handling in Multi-threading
Proper Exception Handling Structure:
List<Task> taskList = new List<Task>();
try
{
for (int i = 0; i < 5; i++)
{
Action<string> action = t =>
{
try
{
Thread.Sleep(100);
if (t.Equals("exception_case"))
{
throw new Exception($"Exception in {t}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Error: {ex.Message}");
}
};
string param = $"param_{i}";
taskList.Add(Task.Run(() => action.Invoke(param)));
}
Task.WaitAll(taskList.ToArray());
}
catch (Exception ex)
{
Console.WriteLine($"Main thread error: {ex.Message}");
}
AggregateException Handling:
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (var exception in ex.InnerExceptions)
{
Console.WriteLine($"{exception.Message} Type: {exception.GetType().Name}");
}
ex.Handle(x =>
{
if (x.Message.Contains("expected_error"))
{
return true; // Don't re-throw
}
else
{
return false; // Re-throw
}
});
}
Task Cancellation
Using CancellationTokenSource:
List<Task> taskList = new List<Task>();
CancellationTokenSource cancellationToken = new CancellationTokenSource();
try
{
for (int i = 0; i < 5; i++)
{
Action<string> action = t =>
{
try
{
if (cancellationToken.Token.IsCancellationRequested)
{
Console.WriteLine($"{t}: Cancellation requested");
return;
}
// Perform work
Thread.Sleep(100);
}
catch (Exception ex)
{
cancellationToken.Cancel();
Console.WriteLine($"Error: {ex.Message}");
}
};
string param = $"param_{i}";
taskList.Add(Task.Run(() => action.Invoke(param), cancellationToken.Token));
}
Task.WaitAll(taskList.ToArray());
}
catch (Exception ex)
{
Console.WriteLine($"Main thread error: {ex.Message}");
}
Cancellation Token Registration:
CancellationTokenSource cancellationToken = new CancellationTokenSource();
cancellationToken.Token.Register(() =>
{
Debug.WriteLine("Cancellation triggered, performing cleanup...");
});
var task = Task.Factory.StartNew(() =>
{
while (!cancellationToken.IsCancellationRequested)
{
Thread.Sleep(100);
Debug.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} running");
}
}, cancellationToken.Token);
Thread.Sleep(1000);
cancellationToken.Cancel();
Variable Capture in Loops
Problem with Loop Variable Capture:
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Console.WriteLine($"Value: {i}"); // Always prints 5
});
}
Correct Approach - Capture Local Variables:
for (int i = 0; i < 5; i++)
{
int capturedValue = i;
Task.Run(() =>
{
Console.WriteLine($"Value: {capturedValue}");
});
}
Thread Safety and Locking Mechanisms
Without Locking (Unsafe):
List<Task> taskList = new List<Task>();
int counter = 0;
for (int i = 0; i < 10000; i++)
{
taskList.Add(Task.Run(() =>
{
counter += 1; // Race condition
}));
}
Task.WhenAll(taskList.ToArray()).ContinueWith((t) =>
{
Console.WriteLine($"Final count: {counter}");
});
With Locking (Safe):
private static readonly object syncLock = new object();
List<Task> taskList = new List<Task>();
int counter = 0;
for (int i = 0; i < 10000; i++)
{
taskList.Add(Task.Run(() =>
{
lock (syncLock)
{
counter += 1;
}
}));
}
Task.WhenAll(taskList.ToArray()).ContinueWith((t) =>
{
Console.WriteLine($"Final count: {counter}");
});
Async/Await Pattern
Basic Async/Await Usage:
private async void ButtonClickHandler(object sender, EventArgs e)
{
Console.WriteLine("Button click started");
await ExecuteWithoutReturnAsync();
Console.WriteLine("Button click completed");
}
private async static Task ExecuteWithoutReturnAsync()
{
await Task.Run(() =>
{
Console.WriteLine("Performing operation");
Thread.Sleep(500);
Console.WriteLine("Operation completed");
});
Console.WriteLine("Async method completed");
}
File I/O with Async/Await:
async static Task<string> ReadFileAsStringAsync()
{
using (FileStream fileStream = new FileStream(Environment.CurrentDirectory + "//data.txt", FileMode.Open))
{
var buffer = new byte[fileStream.Length];
var bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);
var content = Encoding.Default.GetString(buffer, 0, buffer.Length);
return content;
}
}
Various Lock Types
Volatile Keyword:
private static volatile bool stopFlag = false;
static void Main(string[] args)
{
var workerThread = new Thread(() =>
{
while (!stopFlag)
{
// Perform work
}
});
workerThread.Start();
Thread.Sleep(1000);
stopFlag = true;
workerThread.Join();
}
Interlocked Operations:
int sharedCounter = 5;
Interlocked.Increment(ref sharedCounter); // counter = 6
Interlocked.Decrement(ref sharedCounter); // counter = 5
Interlocked.Add(ref sharedCounter, 10); // counter = 15
Interlocked.Exchange(ref sharedCounter, 1); // counter = 1
SpinLock Implementation:
private static SpinLock spinLock = new SpinLock();
static int numberCounter = 0;
static void ProcessWork()
{
try
{
for (int i = 0; i < 100; i++)
{
var lockTaken = false;
spinLock.TryEnter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine(numberCounter++);
}
}
}
finally
{
if (spinLock.IsHeld)
{
spinLock.Exit();
}
}
}
Parallel Processing
Parallel For Loop:
Stopwatch timer = new Stopwatch();
timer.Start();
ConcurrentStack<int> stack = new ConcurrentStack<int>();
Parallel.For(0, 100, (item) =>
{
Console.WriteLine("Processing: " + item);
stack.Push(item);
});
timer.Stop();
Console.WriteLine("Parallel time: " + timer.ElapsedMilliseconds);
Parallel Calculation:
var totalSum = 0;
Parallel.For<int>(1, 100,
() => { return 0; },
(current, loop, subtotal) =>
{
subtotal += current;
return subtotal;
},
(subtotal) =>
{
Interlocked.Add(ref totalSum, subtotal);
});
Console.WriteLine(totalSum);
PLINQ (Parallel LINQ)
Basic PLINQ Usage:
var numbers = Enumerable.Range(0, 100);
var query = from n in numbers.AsParallel()
select new
{
threadId = Thread.CurrentThread.ManagedThreadId,
number = n
};
foreach (var item in query)
{
Console.WriteLine(item);
}
Task Schedulers
Custom Task Scheduler:
public class ThreadPerTaskScheduler : TaskScheduler
{
protected override IEnumerable<Task> GetScheduledTasks()
{
return Enumerable.Empty<Task>();
}
protected override void QueueTask(Task task)
{
var thread = new Thread(() =>
{
TryExecuteTask(task);
});
thread.Start();
}
protected override bool TryExecuteTaskInline(Task task, bool wasQueued)
{
return false;
}
}
Concurrency Collections
ConcurrentBag, ConcurrentStack, ConcurrentQueue:
// ConcurrentBag for thread-local storage
ConcurrentBag<int> bag = new ConcurrentBag<int>();
bag.Add(1);
bag.Add(2);
// Thread-safe stack operations
ConcurrentStack<int> stack = new ConcurrentStack<int>();
stack.Push(42);
stack.TryPop(out int result);
// Thread-safe queue operations
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
queue.Enqueue(100);
queue.TryDequeue(out int dequeuedValue);
Common Threading Issues
High CPU Usage Prevention:
Avoid infinite loops in threads that don't yield control:
// Bad example - causes high CPU
while (true)
{
// Intensive computation without yielding
}
// Better approach
while (true)
{
// Computation
Thread.Sleep(1); // Allow other threads to run
}
Deadlock Prevention:
Avoid nested locks and maintain consistent locking order:
// Risky pattern
lock (lockA)
{
lock (lockB) // Potential deadlock
{
// Work
}
}
Memory Management:
Be cautious with large object allocations in multi-threaded scenarios:
static StringBuilder buffer = new StringBuilder();
// Risky - can cause memory issues
for (int i = 0; i < 10000000; i++)
{
buffer.Append("content");
}