C#
parallel programming
task management
concurrency control
threading

How to limit the maximum number of parallel tasks in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Managing concurrency in C# is a crucial skill for developers, especially when working with applications that perform intense data processing or need to handle multiple parallel tasks simultaneously. The .NET framework provides various tools and libraries to manage concurrency, such as threading, Task Parallel Library (TPL), async/await, and more. This article will focus on how to limit the maximum number of parallel tasks using C#.

Why Limit Parallel Tasks?

Before diving into the technical details, it's important to understand why one might want to limit the number of parallel tasks. Here are a few reasons:

  1. Resource Management: Excessive parallel tasks can exhaust system resources, leading to slower performance or system crashes.
  2. Cost Efficiency: In cloud environments, excessive resource usage can drive up costs significantly.
  3. Stability: Seamless management of concurrently running tasks can prevent deadlocks and race conditions.

Using SemaphoreSlim

One of the efficient ways to control the concurrency is by using SemaphoreSlim. This class is a lightweight, efficient control mechanism that provides a way to limit the number of threads that can access a resource.

Example: Using SemaphoreSlim

Here's an example of how to limit parallel tasks using SemaphoreSlim:

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task Main(string[] args)
8    {
9        int maxParallelTasks = 5;
10        SemaphoreSlim semaphore = new SemaphoreSlim(maxParallelTasks);
11        
12        Task[] tasks = new Task[10];
13        for (int i = 0; i < tasks.Length; i++)
14        {
15            tasks[i] = Task.Run(async () => 
16            {
17                await semaphore.WaitAsync();
18                try
19                {
20                    Console.WriteLine($"Task {Task.CurrentId} is starting");
21                    await Task.Delay(1000); // Simulating work
22                    Console.WriteLine($"Task {Task.CurrentId} is finished");
23                }
24                finally
25                {
26                    semaphore.Release();
27                }
28            });
29        }
30        
31        await Task.WhenAll(tasks);
32    }
33}

Explanation

  1. Initialize SemaphoreSlim: We create an instance of SemaphoreSlim with the constructor taking a parameter for the maximum number of concurrent tasks (maxParallelTasks).
  2. Task Running: For each task, we use semaphore.WaitAsync() to ensure that no more than the specified number of tasks run concurrently.
  3. Release: After the task completes, semaphore.Release() is called to allow another task to start.

Utilizing TaskScheduler

The TaskScheduler in .NET allows for more sophisticated control over task execution. By creating a custom task scheduler, you can define the parallel task limit.

Example: Using TaskScheduler

Creating a custom TaskScheduler that restricts the number of parallel tasks requires some in-depth understanding of the scheduler, but here is a simplified illustration:

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading;
4using System.Threading.Tasks;
5
6public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
7{
8    private readonly LinkedList<Task> _tasks = new LinkedList<Task>();
9    private readonly int _maxDegreeOfParallelism;
10    private int _runningTasks = 0;
11
12    public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
13    {
14        if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
15        _maxDegreeOfParallelism = maxDegreeOfParallelism;
16    }
17
18    protected override void QueueTask(Task task)
19    {
20        lock (_tasks)
21        {
22            _tasks.AddLast(task);
23            if (_runningTasks < _maxDegreeOfParallelism)
24            {
25                _runningTasks++;
26                StartNextTask();
27            }
28        }
29    }
30
31    private void StartNextTask()
32    {
33        Task toRun = null;
34        lock (_tasks)
35        {
36            if (_tasks.Count > 0)
37            {
38                toRun = _tasks.First.Value;
39                _tasks.RemoveFirst();
40            }
41        }
42        if (toRun != null)
43        {
44            ThreadPool.UnsafeQueueUserWorkItem(_ => { TryExecuteTask(toRun); StartNextTask(); }, null);
45        }
46        else
47        {
48            Interlocked.Decrement(ref _runningTasks);
49        }
50    }
51
52    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
53    protected override IEnumerable<Task> GetScheduledTasks() => _tasks;
54
55    public override int MaximumConcurrencyLevel => _maxDegreeOfParallelism;
56}

Explanation

  1. Task Queue: The custom scheduler maintains a queue of tasks to ensure they execute under the specified concurrency limit.
  2. QueueTask: This method adds tasks to the queue and checks if new tasks can start based on the defined limit.
  3. StartNextTask: Responsible for executing and dequeueing tasks as long as they fit within the defined limit.

Summary Table

Here is a summary of key concepts discussed:

ConceptDescription
SemaphoreSlimLightweight control over the number of concurrent threads accessing a resource.
Semaphore Wait/ReleaseMethods to ensure a task waits for resource availability and releases upon completion.
TaskSchedulerProvides control over task execution, with the ability to finely manage concurrency.
Custom SchedulerAllows defining custom rules for task execution to limit concurrency at the scheduling level.

Conclusion

Limiting the number of parallel tasks in C# is imperative for optimal resource utilization and ensuring the stability of your applications. By leveraging SemaphoreSlim and custom TaskScheduler, developers can exercise fine control over concurrency. Experiment with these techniques to find a balance that fits your application's unique requirements.


Course illustration
Course illustration

All Rights Reserved.