Introduction
Welcome to Module 8 of our Complete C# Course: From Beginner to Advanced! After mastering file handling and serialization in Module 7, it’s time to tackle asynchronous and parallel programming to build high-performance, responsive applications. In this module, we’ll cover multithreading basics, Tasks and Task Parallel Library (TPL), async and await keywords, Parallel.ForEach and PLINQ, cancellation tokens, and deadlocks and synchronization. We’ll apply these concepts in a practical image processing system for a media app, inspired by real-world scenarios like batch processing or data analysis. With detailed examples, best practices, and pros/cons, you’ll learn to optimize C# applications for performance. Let’s dive in!
1. Multithreading Basics
Multithreading allows multiple threads to run concurrently, improving performance for CPU-bound or I/O-bound tasks.
Example: Basic Threading
using System;
using System.Threading;
namespace ImageProcessingSystem
{
class Program
{
static void ProcessImage(string imageName)
{
Console.WriteLine($"Processing {imageName} on thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // Simulate work
Console.WriteLine($"{imageName} processed.");
}
static void Main(string[] args)
{
Thread thread1 = new Thread(() => ProcessImage("Image1.jpg"));
Thread thread2 = new Thread(() => ProcessImage("Image2.jpg"));
thread1.Start();
thread2.Start();
thread1.Join(); // Wait for threads to complete
thread2.Join();
Console.WriteLine("All images processed.");
}
}
}
Real-World Use: Processing multiple images or files concurrently in a media app.
Pros:
Improves performance for parallel tasks.
Direct control over thread creation and management.
Cons:
Manual thread management is error-prone.
Thread creation is resource-intensive.
Best Practices:
Use threads for simple, isolated tasks.
Always call Join or handle thread completion.
Avoid excessive thread creation to prevent overhead.
Alternatives:
Task Parallel Library (TPL) for higher-level abstractions.
ThreadPool for lightweight thread management.
2. Tasks and Task Parallel Library (TPL)
Tasks provide a higher-level abstraction over threads, managed by the Task Parallel Library (TPL) for easier concurrency.
Example: Task-Based Image Processing
using System;
using System.Threading.Tasks;
namespace ImageProcessingSystem
{
class Program
{
static Task ProcessImageAsync(string imageName)
{
return Task.Run(() =>
{
Console.WriteLine($"Processing {imageName} on thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // Simulate work
Console.WriteLine($"{imageName} processed.");
});
}
static void Main(string[] args)
{
Task[] tasks = new Task[]
{
ProcessImageAsync("Image1.jpg"),
ProcessImageAsync("Image2.jpg")
};
Task.WaitAll(tasks); // Wait for all tasks
Console.WriteLine("All images processed.");
}
}
}
Real-World Use: Batch processing files or API calls in parallel.
Pros:
Simplifies thread management with Task abstraction.
TPL optimizes thread usage via ThreadPool.
Supports task continuation and error handling.
Cons:
Higher abstraction can obscure thread behavior.
Task overhead for very small operations.
Best Practices:
Use Task.Run for CPU-bound work.
Use Task.WaitAll for synchronous waiting.
Handle exceptions in tasks to avoid unobserved errors.
Alternatives:
Raw threads for fine-grained control.
Async/await for I/O-bound tasks.
3. async and await Keywords
async and await enable asynchronous programming for I/O-bound operations, improving responsiveness.
Example: Async File Download
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace ImageProcessingSystem
{
class Program
{
static async Task DownloadImageAsync(string url, string filePath)
{
using (HttpClient client = new HttpClient())
{
Console.WriteLine($"Downloading {url}...");
byte[] data = await client.GetByteArrayAsync(url);
await File.WriteAllBytesAsync(filePath, data);
Console.WriteLine($"{url} downloaded to {filePath}.");
}
}
static async Task Main(string[] args)
{
await DownloadImageAsync("https://example.com/image1.jpg", "image1.jpg");
await DownloadImageAsync("https://example.com/image2.jpg", "image2.jpg");
Console.WriteLine("All downloads complete.");
}
}
}
Real-World Use: Downloading images or fetching API data without blocking the UI.
Pros:
Simplifies asynchronous code with synchronous-like syntax.
Improves responsiveness for I/O-bound tasks.
Cons:
Not suitable for CPU-bound tasks (use Task.Run).
Async misuse can lead to deadlocks.
Best Practices:
Use async Task for methods returning tasks.
Avoid async void except for event handlers.
Use ConfigureAwait(false) in library code for performance.
Alternatives:
Callbacks or continuations for older async patterns.
Synchronous I/O for simple scenarios.
4. Parallel.ForEach and Parallel LINQ (PLINQ)
Parallel.ForEach and PLINQ enable parallel processing of collections for CPU-bound tasks.
Example: Parallel Image Processing
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ImageProcessingSystem
{
class Program
{
static void ProcessImage(string imageName)
{
Console.WriteLine($"Processing {imageName} on thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // Simulate work
Console.WriteLine($"{imageName} processed.");
}
static void Main(string[] args)
{
List<string> images = new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" };
// Parallel.ForEach
Parallel.ForEach(images, image =>
{
ProcessImage(image);
});
// PLINQ
var processedImages = images.AsParallel()
.Select(image =>
{
ProcessImage(image);
return image;
})
.ToList();
Console.WriteLine("All images processed.");
}
}
}
Real-World Use: Batch processing images, resizing videos, or analyzing data.
Pros:
Simplifies parallel iteration over collections.
PLINQ integrates with LINQ for declarative queries.
Cons:
Overhead for small datasets.
Non-deterministic order in PLINQ unless specified.
Best Practices:
Use Parallel.ForEach for simple loops, PLINQ for query-like operations.
Use WithDegreeOfParallelism to limit threads.
Ensure thread-safe operations in parallel code.
Alternatives:
Task for manual parallelization.
Sequential loops for small datasets.
5. Cancellation Tokens
Cancellation tokens allow graceful cancellation of asynchronous or parallel operations.
Example: Cancel Image Processing
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ImageProcessingSystem
{
class Program
{
static async Task ProcessImageAsync(string imageName, CancellationToken token)
{
try
{
Console.WriteLine($"Processing {imageName}...");
await Task.Delay(2000, token); // Simulate work
Console.WriteLine($"{imageName} processed.");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{imageName} cancelled.");
}
}
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task processTask = ProcessImageAsync("Image1.jpg", cts.Token);
Console.WriteLine("Press any key to cancel...");
Console.ReadKey();
cts.Cancel();
await processTask;
Console.WriteLine("Operation complete.");
}
}
}
Real-World Use: Cancelling long-running downloads or batch jobs in user-driven apps.
Pros:
Graceful cancellation without abrupt termination.
Integrates with async and TPL.
Cons:
Requires explicit token checks in code.
Adds complexity to method signatures.
Best Practices:
Pass CancellationToken to async methods and tasks.
Handle OperationCanceledException gracefully.
Use CancellationTokenSource for centralized cancellation.
Alternatives:
Manual flags for cancellation (less robust).
Timeout mechanisms for simpler scenarios.
6. Deadlocks and Synchronization
Deadlocks occur when threads wait on each other indefinitely. Synchronization controls access to shared resources.
Example: Synchronized Image Cache
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ImageProcessingSystem
{
class ImageCache
{
private Dictionary<string, string> cache = new Dictionary<string, string>();
private object lockObject = new object();
public void AddImage(string imageName, string data)
{
lock (lockObject)
{
if (!cache.ContainsKey(imageName))
{
Console.WriteLine($"Caching {imageName} on thread {Thread.CurrentThread.ManagedThreadId}");
cache[imageName] = data;
}
}
}
public string GetImage(string imageName)
{
lock (lockObject)
{
return cache.TryGetValue(imageName, out string data) ? data : null;
}
}
}
class Program
{
static async Task Main(string[] args)
{
ImageCache cache = new ImageCache();
List<Task> tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
string imageName = $"Image{i}.jpg";
tasks.Add(Task.Run(() => cache.AddImage(imageName, $"Data for {imageName}")));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Cached: {cache.GetImage("Image1.jpg")}");
}
}
}
Real-World Use: Managing shared resources like caches or database connections.
Pros:
lock ensures thread-safe access.
Prevents data corruption in multi-threaded apps.
Cons:
Deadlocks can occur with improper lock ordering.
Synchronization adds performance overhead.
Best Practices:
Use lock for simple synchronization.
Avoid nested locks to prevent deadlocks.
Use Monitor.TryEnter for timeout-based locking.
Alternatives:
ConcurrentDictionary for thread-safe collections.
Semaphores or mutexes for advanced scenarios.
Interactive Example: Image Processing System
Let’s build a console-based image processing system to apply asynchronous and parallel programming concepts.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
// File: Models/Image.cs
namespace ImageProcessingSystem.Models
{
public record Image(string Name, double SizeMB);
}
// File: Services/ImageProcessor.cs
namespace ImageProcessingSystem.Services
{
class ImageProcessor
{
private readonly object lockObject = new object();
private List<Models.Image> processedImages = new List<Models.Image>();
public async Task ProcessImageAsync(Models.Image image, CancellationToken token)
{
try
{
Console.WriteLine($"Processing {image.Name} ({image.SizeMB}MB) on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay((int)(image.SizeMB * 1000), token); // Simulate processing
lock (lockObject)
{
processedImages.Add(image);
}
Console.WriteLine($"{image.Name} processed.");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{image.Name} processing cancelled.");
}
}
public async Task ProcessImagesInParallelAsync(List<Models.Image> images, CancellationToken token)
{
await Parallel.ForEachAsync(images, token, async (image, ct) =>
{
await ProcessImageAsync(image, ct);
});
}
public IEnumerable<Models.Image> GetLargeImages(double minSizeMB)
{
return processedImages.AsParallel()
.Where(img => img.SizeMB >= minSizeMB)
.OrderBy(img => img.SizeMB);
}
}
}
// File: Program.cs
namespace ImageProcessingSystem
{
class Program
{
static async Task Main(string[] args)
{
Services.ImageProcessor processor = new Services.ImageProcessor();
CancellationTokenSource cts = new CancellationTokenSource();
List<Models.Image> images = new List<Models.Image>
{
new Models.Image("Image1.jpg", 1.5),
new Models.Image("Image2.jpg", 2.0),
new Models.Image("Image3.jpg", 0.5)
};
while (true)
{
Console.WriteLine("\nImage Processing System");
Console.WriteLine("1. Process Images");
Console.WriteLine("2. View Large Images (>=1MB)");
Console.WriteLine("3. Cancel Processing");
Console.WriteLine("4. Exit");
Console.Write("Choose an option: ");
string choice = Console.ReadLine();
if (choice == "4") break;
switch (choice)
{
case "1":
Task processingTask = processor.ProcessImagesInParallelAsync(images, cts.Token);
Console.WriteLine("Processing started. Press any key to continue...");
Console.ReadKey();
await processingTask;
break;
case "2":
var largeImages = processor.GetLargeImages(1.0);
foreach (var img in largeImages)
{
Console.WriteLine($"{img.Name}: {img.SizeMB}MB");
}
if (!largeImages.Any())
Console.WriteLine("No large images found.");
break;
case "3":
cts.Cancel();
cts = new CancellationTokenSource(); // Reset for next run
Console.WriteLine("Cancellation requested.");
break;
default:
Console.WriteLine("Invalid option!");
break;
}
}
}
}
}
How It Works:
Multithreading: Handled by TPL and Parallel.ForEachAsync.
Tasks/TPL: Task and Parallel.ForEachAsync process images concurrently.
async/await: Used for non-blocking image processing.
Parallel.ForEach/PLINQ: Processes images in parallel and filters large images.
Cancellation Tokens: Allows cancelling long-running tasks.
Synchronization: lock ensures thread-safe updates to processedImages.
Why It’s Useful: Mimics media processing apps like Photoshop or cloud-based image services.
Setup: Create a new Console App in Visual Studio or run dotnet new console. Organize files as shown. Requires .NET 6+ for Parallel.ForEachAsync.
Best Standards for Module 8
Multithreading: Use for CPU-bound tasks; prefer TPL over raw threads.
Tasks/TPL: Use Task for parallel work; handle exceptions properly.
async/await: Use for I/O-bound tasks; avoid blocking with Task.Run.
Parallel.ForEach/PLINQ: Use for parallel collection processing; limit parallelism for resource control.
Cancellation Tokens: Pass to all async/parallel operations; handle cancellation gracefully.
Synchronization: Use lock or concurrent collections; avoid nested locks to prevent deadlocks.
Conclusion
You’ve just mastered asynchronous and parallel programming in C#! By learning multithreading, Tasks, TPL, async/await, Parallel.ForEach, PLINQ, cancellation tokens, and synchronization, you’re equipped to build high-performance, responsive applications. The image processing system demonstrates how these concepts power real-world solutions.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam