Md Mominul Islam | Software and Data Enginnering | SQL Server, .NET, Power BI, Azure Blog

while(!(succeed=try()));

LinkedIn Portfolio Banner

Latest

Home Top Ad

Responsive Ads Here

Sunday, August 17, 2025

Complete C# Course: From Beginner to Advanced – Module 8: Asynchronous and Parallel Programming

 

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