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

Post Top Ad

Responsive Ads Here

Friday, August 22, 2025

Module 6: Mastering Concurrency & Multithreading Patterns in .NET: Thread-Safe Singleton, Producer-Consumer, Reactor, Scheduler, and Futures

 

Table of Contents

  1. Introduction to Concurrency & Multithreading Patterns

    • 1.1 Why Concurrency Matters in Modern Applications

    • 1.2 Overview of Patterns Covered

    • 1.3 Real-World Relevance in .NET Applications

  2. Thread-Safe Singleton Pattern

    • 2.1 What is the Thread-Safe Singleton Pattern?

    • 2.2 Real-World Use Cases

    • 2.3 Implementation in C#

    • 2.4 Exception Handling

    • 2.5 Pros, Cons, and Alternatives

    • 2.6 Best Practices

  3. Producer-Consumer Pattern

    • 3.1 Understanding the Producer-Consumer Pattern

    • 3.2 Real-World Scenarios

    • 3.3 Implementation with C# and BlockingCollection

    • 3.4 Exception Handling

    • 3.5 Pros, Cons, and Alternatives

    • 3.6 Best Practices

  4. Reactor Pattern

    • 4.1 Introduction to the Reactor Pattern

    • 4.2 Real-World Applications

    • 4.3 Implementation in ASP.NET

    • 4.4 Exception Handling

    • 4.5 Pros, Cons, and Alternatives

    • 4.6 Best Practices

  5. Scheduler/Task Queue Patterns

    • 5.1 What are Scheduler and Task Queue Patterns?

    • 5.2 Real-World Use Cases

    • 5.3 Implementation with C# and SQL Server

    • 5.4 Exception Handling

    • 5.5 Pros, Cons, and Alternatives

    • 5.6 Best Practices

  6. Futures and Promises (Task in .NET)

    • 6.1 Understanding Futures and Promises

    • 6.2 Real-World Scenarios

    • 6.3 Implementation with C# Task

    • 6.4 Exception Handling

    • 6.5 Pros, Cons, and Alternatives

    • 6.6 Best Practices

  7. Comparing Patterns: When to Use Which

  8. Conclusion and Next Steps


1. Introduction to Concurrency & Multithreading Patterns

1.1 Why Concurrency Matters in Modern Applications

In today’s fast-paced digital world, applications must handle multiple tasks simultaneously to ensure responsiveness and scalability. Concurrency and multithreading patterns enable developers to optimize resource usage, improve performance, and manage complex operations in .NET applications. Whether it’s processing user requests in an ASP.NET web app or handling background tasks with SQL Server, mastering these patterns is crucial for building robust systems.

1.2 Overview of Patterns Covered

This module covers five essential concurrency patterns:

  • Thread-Safe Singleton: Ensures a single instance of a class in a multithreaded environment.

  • Producer-Consumer: Manages task distribution between producers and consumers.

  • Reactor: Handles multiple concurrent events efficiently.

  • Scheduler/Task Queue: Manages task execution with prioritization and queuing.

  • Futures and Promises: Simplifies asynchronous programming with .NET Tasks.

1.3 Real-World Relevance in .NET Applications

From e-commerce platforms to real-time analytics dashboards, these patterns are used in .NET applications to handle high loads, ensure thread safety, and optimize performance. For example, an ASP.NET application might use the Producer-Consumer pattern to process user orders while a Scheduler manages background jobs like sending emails.


2. Thread-Safe Singleton Pattern

2.1 What is the Thread-Safe Singleton Pattern?

The Singleton pattern ensures a class has only one instance and provides a global access point. In multithreaded environments, thread-safe implementations prevent multiple instances from being created due to concurrent access.

2.2 Real-World Use Cases

  • Configuration Manager: A single instance manages application settings across threads in an ASP.NET app.

  • Logging Service: Ensures all threads write to a single log file or database.

  • Database Connection Pool: Manages a single pool of connections in a multithreaded server.

2.3 Implementation in C#

Here’s a thread-safe Singleton implementation using the Lazy class in C#, ideal for ASP.NET applications.

using System;

public sealed class ConfigurationManager
{
    private static readonly Lazy<ConfigurationManager> instance = 
        new Lazy<ConfigurationManager>(() => new ConfigurationManager(), isThreadSafe: true);

    private ConfigurationManager()
    {
        // Private constructor to prevent instantiation
        Console.WriteLine("ConfigurationManager initialized.");
    }

    public static ConfigurationManager Instance => instance.Value;

    public string GetConfig(string key)
    {
        // Simulate fetching configuration from a source
        return $"Value for {key}";
    }
}

// Usage in ASP.NET Controller
public class ConfigController : ControllerBase
{
    [HttpGet("config/{key}")]
    public IActionResult GetConfig(string key)
    {
        var configValue = ConfigurationManager.Instance.GetConfig(key);
        return Ok(configValue);
    }
}

2.4 Exception Handling

Handle potential exceptions during initialization:

public sealed class ConfigurationManager
{
    private static readonly Lazy<ConfigurationManager> instance = 
        new Lazy<ConfigurationManager>(() => 
        {
            try
            {
                return new ConfigurationManager();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to initialize ConfigurationManager: {ex.Message}");
                throw;
            }
        }, isThreadSafe: true);

    private ConfigurationManager()
    {
        try
        {
            // Initialize resources (e.g., load config from file or database)
            Console.WriteLine("ConfigurationManager initialized.");
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("ConfigurationManager initialization failed.", ex);
        }
    }

    public static ConfigurationManager Instance => instance.Value;

    public string GetConfig(string key)
    {
        try
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException(nameof(key));
            return $"Value for {key}";
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error fetching config: {ex.Message}");
            throw;
        }
    }
}

2.5 Pros, Cons, and Alternatives

  • Pros:

    • Ensures a single instance, reducing resource usage.

    • Lazy initialization improves performance.

    • Thread-safe with minimal overhead using Lazy.

  • Cons:

    • Can become a bottleneck in highly concurrent systems.

    • Difficult to unit test due to global state.

  • Alternatives:

    • Dependency Injection (DI): Use DI containers (e.g., ASP.NET Core’s built-in DI) to manage single instances.

    • Static Classes: For simple scenarios without instance state.

2.6 Best Practices

  • Use Lazy for thread-safe lazy initialization.

  • Avoid complex logic in the constructor to prevent deadlocks.

  • Consider DI for better testability in ASP.NET applications.


3. Producer-Consumer Pattern

3.1 Understanding the Producer-Consumer Pattern

The Producer-Consumer pattern decouples task creation (producers) from task processing (consumers) using a shared queue. Producers add tasks, and consumers process them asynchronously, ideal for load balancing.

3.2 Real-World Scenarios

  • Order Processing: An e-commerce app where producers add orders to a queue, and consumers process payments.

  • Log Processing: Producers log events, and consumers write to a SQL Server database.

  • Image Processing: Producers upload images, and consumers resize or compress them.

3.3 Implementation with C# and BlockingCollection

Here’s an example using BlockingCollection to process orders in an ASP.NET app.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public class OrderProcessor
{
    private readonly BlockingCollection<Order> orderQueue = new BlockingCollection<Order>(100);

    public void Produce(Order order)
    {
        orderQueue.Add(order);
    }

    public void StartProcessing(int consumerCount)
    {
        for (int i = 0; i < consumerCount; i++)
        {
            Task.Factory.StartNew(() =>
            {
                foreach (var order in orderQueue.GetConsumingEnumerable())
                {
                    ProcessOrder(order);
                }
            }, TaskCreationOptions.LongRunning);
        }
    }

    private void ProcessOrder(Order order)
    {
        // Simulate processing (e.g., save to SQL Server)
        Console.WriteLine($"Processing Order {order.Id} for {order.CustomerName}");
        Thread.Sleep(1000); // Simulate work
    }
}

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
}

// Usage in ASP.NET Controller
public class OrderController : ControllerBase
{
    private readonly OrderProcessor _processor;

    public OrderController(OrderProcessor processor)
    {
        _processor = processor;
    }

    [HttpPost("order")]
    public IActionResult AddOrder([FromBody] Order order)
    {
        _processor.Produce(order);
        return Ok("Order queued for processing.");
    }
}

3.4 Exception Handling

Add robust error handling:

public class OrderProcessor
{
    private readonly BlockingCollection<Order> orderQueue = new BlockingCollection<Order>(100);

    public void Produce(Order order)
    {
        try
        {
            if (order == null || order.Id <= 0)
                throw new ArgumentException("Invalid order.");
            orderQueue.Add(order);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error producing order: {ex.Message}");
            throw;
        }
    }

    public void StartProcessing(int consumerCount)
    {
        try
        {
            if (consumerCount <= 0)
                throw new ArgumentException("Consumer count must be positive.");
            for (int i = 0; i < consumerCount; i++)
            {
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        foreach (var order in orderQueue.GetConsumingEnumerable())
                        {
                            ProcessOrder(order);
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        Console.WriteLine("Consumer cancelled.");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Consumer error: {ex.Message}");
                    }
                }, TaskCreationOptions.LongRunning);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error starting consumers: {ex.Message}");
            throw;
        }
    }

    private void ProcessOrder(Order order)
    {
        try
        {
            // Simulate saving to SQL Server
            Console.WriteLine($"Processing Order {order.Id} for {order.CustomerName}");
            Thread.Sleep(1000);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error processing order {order.Id}: {ex.Message}");
            throw;
        }
    }
}

3.5 Pros, Cons, and Alternatives

  • Pros:

    • Decouples producers and consumers for scalability.

    • Built-in thread safety with BlockingCollection.

    • Balances load across multiple consumers.

  • Cons:

    • Queue size management can be complex.

    • Potential for bottlenecks if consumers are slow.

  • Alternatives:

    • Channels (System.Threading.Channels): For high-performance, async scenarios.

    • Message Queues (e.g., RabbitMQ): For distributed systems.

3.6 Best Practices

  • Use bounded queues to prevent memory issues.

  • Monitor queue length to avoid overload.

  • Implement cancellation tokens for graceful shutdown.

  • Log errors for debugging and monitoring.


4. Reactor Pattern

4.1 Introduction to the Reactor Pattern

The Reactor pattern handles multiple concurrent events by dispatching them to appropriate handlers. It’s ideal for event-driven systems like web servers.

4.2 Real-World Applications

  • Web Servers: ASP.NET Core handling HTTP requests.

  • Real-Time Notifications: Processing user events in a chat application.

  • IoT Systems: Managing sensor data streams.

4.3 Implementation in ASP.NET

Here’s a simplified Reactor implementation for handling HTTP requests in ASP.NET Core.

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

public interface IEventHandler
{
    Task HandleAsync(Event evt);
}

public class Event
{
    public string Type { get; set; }
    public string Data { get; set; }
}

public class NotificationHandler : IEventHandler
{
    public async Task HandleAsync(Event evt)
    {
        // Simulate notification processing
        await Task.Delay(100);
        Console.WriteLine($"Handled notification: {evt.Data}");
    }
}

public class Reactor
{
    private readonly ConcurrentDictionary<string, IEventHandler> handlers = new();

    public void RegisterHandler(string eventType, IEventHandler handler)
    {
        handlers[eventType] = handler;
    }

    public async Task DispatchAsync(Event evt)
    {
        if (handlers.TryGetValue(evt.Type, out var handler))
        {
            await handler.HandleAsync(evt);
        }
    }
}

// Usage in ASP.NET Controller
public class EventController : ControllerBase
{
    private readonly Reactor _reactor;

    public EventController(Reactor reactor)
    {
        _reactor = reactor;
        _reactor.RegisterHandler("notification", new NotificationHandler());
    }

    [HttpPost("event")]
    public async Task<IActionResult> ProcessEvent([FromBody] Event evt)
    {
        await _reactor.DispatchAsync(evt);
        return Ok("Event processed.");
    }
}

4.4 Exception Handling

Add error handling:

public class Reactor
{
    private readonly ConcurrentDictionary<string, IEventHandler> handlers = new();

    public void RegisterHandler(string eventType, IEventHandler handler)
    {
        try
        {
            if (string.IsNullOrEmpty(eventType))
                throw new ArgumentNullException(nameof(eventType));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
            handlers[eventType] = handler;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error registering handler: {ex.Message}");
            throw;
        }
    }

    public async Task DispatchAsync(Event evt)
    {
        try
        {
            if (evt == null || string.IsNullOrEmpty(evt.Type))
                throw new ArgumentException("Invalid event.");
            if (handlers.TryGetValue(evt.Type, out var handler))
            {
                await handler.HandleAsync(evt);
            }
            else
            {
                Console.WriteLine($"No handler for event type: {evt.Type}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error dispatching event: {ex.Message}");
            throw;
        }
    }
}

4.5 Pros, Cons, and Alternatives

  • Pros:

    • Scalable for handling multiple event types.

    • Decouples event sources from handlers.

    • Fits naturally with ASP.NET’s event-driven model.

  • Cons:

    • Complex to manage many handlers.

    • Potential performance overhead for high-frequency events.

  • Alternatives:

    • Observer Pattern: For simpler event handling.

    • Message Brokers (e.g., Kafka): For distributed systems.

4.6 Best Practices

  • Use concurrent collections for thread safety.

  • Implement logging for unhandled events.

  • Avoid blocking operations in handlers.


5. Scheduler/Task Queue Patterns

5.1 What are Scheduler and Task Queue Patterns?

The Scheduler/Task Queue pattern manages task execution by prioritizing and queuing tasks, often persisting them in a database like SQL Server for reliability.

5.2 Real-World Use Cases

  • Background Jobs: Sending emails or generating reports in an ASP.NET app.

  • Task Scheduling: Running nightly data imports.

  • Job Queuing: Processing large datasets in batches.

5.3 Implementation with C# and SQL Server

Here’s an example using SQL Server to store tasks and a scheduler to process them.

using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;

public class TaskEntity
{
    public int Id { get; set; }
    public string TaskType { get; set; }
    public string Payload { get; set; }
    public DateTime ScheduledTime { get; set; }
    public bool IsProcessed { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<TaskEntity> Tasks { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}

public class TaskScheduler
{
    private readonly AppDbContext _context;

    public TaskScheduler(AppDbContext context)
    {
        _context = context;
    }

    public async Task EnqueueTaskAsync(TaskEntity task)
    {
        _context.Tasks.Add(task);
        await _context.SaveChangesAsync();
    }

    public async Task ProcessTasksAsync()
    {
        while (true)
        {
            var task = await _context.Tasks
                .Where(t => !t.IsProcessed && t.ScheduledTime <= DateTime.UtcNow)
                .FirstOrDefaultAsync();

            if (task == null)
            {
                await Task.Delay(1000);
                continue;
            }

            // Process task
            Console.WriteLine($"Processing task {task.Id}: {task.Payload}");
            task.IsProcessed = true;
            await _context.SaveChangesAsync();
        }
    }
}

// Usage in ASP.NET
public class TaskController : ControllerBase
{
    private readonly TaskScheduler _scheduler;

    public TaskController(TaskScheduler scheduler)
    {
        _scheduler = scheduler;
    }

    [HttpPost("task")]
    public async Task<IActionResult> ScheduleTask([FromBody] TaskEntity task)
    {
        await _scheduler.EnqueueTaskAsync(task);
        return Ok("Task scheduled.");
    }
}

5.4 Exception Handling

Add error handling:

public class TaskScheduler
{
    private readonly AppDbContext _context;

    public TaskScheduler(AppDbContext context)
    {
        _context = context;
    }

    public async Task EnqueueTaskAsync(TaskEntity task)
    {
        try
        {
            if (task == null || string.IsNullOrEmpty(task.TaskType))
                throw new ArgumentException("Invalid task.");
            _context.Tasks.Add(task);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException ex)
        {
            Console.WriteLine($"Database error while enqueuing task: {ex.Message}");
            throw;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error enqueuing task: {ex.Message}");
            throw;
        }
    }

    public async Task ProcessTasksAsync()
    {
        while (true)
        {
            try
            {
                var task = await _context.Tasks
                    .Where(t => !t.IsProcessed && t.ScheduledTime <= DateTime.UtcNow)
                    .FirstOrDefaultAsync();

                if (task == null)
                {
                    await Task.Delay(1000);
                    continue;
                }

                try
                {
                    Console.WriteLine($"Processing task {task.Id}: {task.Payload}");
                    task.IsProcessed = true;
                    await _context.SaveChangesAsync();
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error processing task {task.Id}: {ex.Message}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error fetching tasks: {ex.Message}");
                await Task.Delay(5000); // Prevent tight loop on error
            }
        }
    }
}

5.5 Pros, Cons, and Alternatives

  • Pros:

    • Persistent tasks ensure reliability.

    • Scalable for distributed systems.

    • Flexible scheduling with SQL Server.

  • Cons:

    • Database dependency adds latency.

    • Polling can be resource-intensive.

  • Alternatives:

    • Hangfire: For robust background job processing.

    • Quartz.NET: For advanced scheduling.

5.6 Best Practices

  • Use a dedicated table for tasks.

  • Implement retry logic for failed tasks.

  • Optimize database queries to avoid performance bottlenecks.


6. Futures and Promises (Task in .NET)

6.1 Understanding Futures and Promises

Futures and Promises represent asynchronous operations that will complete in the future. In .NET, the Task class provides a powerful implementation.

6.2 Real-World Scenarios

  • Async API Calls: Fetching data from external services in ASP.NET.

  • Parallel Processing: Running multiple computations concurrently.

  • Background Tasks: Sending emails without blocking the UI.

6.3 Implementation with C# Task

Here’s an example of fetching user data asynchronously.

using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;

public class UserService
{
    public async Task<User> GetUserAsync(int userId)
    {
        // Simulate async data fetch
        await Task.Delay(1000);
        return new User { Id = userId, Name = $"User {userId}" };
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class UserController : ControllerBase
{
    private readonly UserService _userService;

    public UserController(UserService userService)
    {
        _userService = userService;
    }

    [HttpGet("user/{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var user = await _userService.GetUserAsync(id);
        return Ok(user);
    }

    [HttpGet("users")]
    public async Task<IActionResult> GetMultipleUsers()
    {
        var tasks = new[]
        {
            _userService.GetUserAsync(1),
            _userService.GetUserAsync(2),
            _userService.GetUserAsync(3)
        };
        var users = await Task.WhenAll(tasks);
        return Ok(users);
    }
}

6.4 Exception Handling

Handle async errors:

public class UserService
{
    public async Task<User> GetUserAsync(int userId)
    {
        try
        {
            if (userId <= 0)
                throw new ArgumentException("Invalid user ID.");
            await Task.Delay(1000); // Simulate async work
            return new User { Id = userId, Name = $"User {userId}" };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error fetching user {userId}: {ex.Message}");
            throw;
        }
    }
}

public class UserController : ControllerBase
{
    private readonly UserService _userService;

    public UserController(UserService userService)
    {
        _userService = userService;
    }

    [HttpGet("user/{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        try
        {
            var user = await _userService.GetUserAsync(id);
            return Ok(user);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error in GetUser: {ex.Message}");
            return StatusCode(500, "Error fetching user.");
        }
    }

    [HttpGet("users")]
    public async Task<IActionResult> GetMultipleUsers()
    {
        try
        {
            var tasks = new[]
            {
                _userService.GetUserAsync(1),
                _userService.GetUserAsync(2),
                _userService.GetUserAsync(3)
            };
            var users = await Task.WhenAll(tasks);
            return Ok(users);
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($"Error fetching users: {ex.Message}");
            return StatusCode(500, "Error fetching users.");
        }
    }
}

6.5 Pros, Cons, and Alternatives

  • Pros:

    • Simplifies asynchronous programming.

    • Built-in support in .NET with Task.

    • Enables parallel execution with Task.WhenAll.

  • Cons:

    • Complex error handling with AggregateException.

    • Overuse can lead to resource exhaustion.

  • Alternatives:

    • ValueTask: For high-performance, short-lived async operations.

    • Async Streams: For streaming data asynchronously.

6.6 Best Practices

  • Use async/await consistently to avoid blocking.

  • Handle AggregateException for Task.WhenAll.

  • Use ValueTask for performance-critical paths.


7. Comparing Patterns: When to Use Which

Pattern

Best For

Key Considerations

Thread-Safe Singleton

Global resource management

Avoid overuse; consider DI

Producer-Consumer

Task distribution and load balancing

Manage queue size; monitor performance

Reactor

Event-driven systems

Ensure scalable handler management

Scheduler/Task Queue

Background job processing

Optimize database access; use retries

Futures/Promises

Asynchronous operations

Handle exceptions carefully


8. Conclusion and Next Steps

Mastering concurrency patterns like Thread-Safe Singleton, Producer-Consumer, Reactor, Scheduler, and Futures/Promises empowers developers to build scalable, responsive .NET applications. By combining these patterns with best practices and robust exception handling, you can tackle complex multithreading challenges in ASP.NET and SQL Server-based systems. Explore further by experimenting with the provided code samples and integrating these patterns into your projects. Stay tuned for the next module in our Software Design Patterns Course!

No comments:

Post a Comment

Thanks for your valuable comment...........
Md. Mominul Islam

Post Bottom Ad

Responsive Ads Here