Back to: C#.NET Tutorials For Beginners and Professionals
How to Limit the Number of Concurrent Tasks in C#
In this article, I am going to discuss How to Limit the Number of Concurrent Tasks in C# using SemaphoreSlim with Examples. Please read our previous article where we discussed How to Execute Multiple Tasks using the WhenAll Method in C# with Examples. At the end of this article, you will understand the following two pointers in depth.
- How to Limit the Number of Concurrent Tasks in C#?
- How to Handle the Response of Multiple Tasks when executed using Task.WhenAll Method?
How to Limit the Number of Concurrent Tasks in C#?
In the below example, we are processing 100000 tasks concurrently.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; namespace AsynchronousProgramming { class Program { static void Main(string[] args) { var stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine($"Main Thread Started"); List<CreditCard> creditCards = CreditCard.GenerateCreditCards(100000); Console.WriteLine($"Credit Card Generated : {creditCards.Count}"); ProcessCreditCards(creditCards); Console.WriteLine($"Main Thread Completed"); stopwatch.Start(); Console.WriteLine($"Main Thread Execution Time {stopwatch.ElapsedMilliseconds / 1000.0} Seconds"); Console.ReadKey(); } public static async void ProcessCreditCards(List<CreditCard> creditCards) { var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task<string>>(); await Task.Run(() => { foreach (var creditCard in creditCards) { var response = ProcessCard(creditCard); tasks.Add(response); } }); //It will execute all the tasks concurrently await Task.WhenAll(tasks); stopwatch.Stop(); Console.WriteLine($"Processing of {creditCards.Count} Credit Cards Done in {stopwatch.ElapsedMilliseconds/1000.0} Seconds"); } public static async Task<string> ProcessCard(CreditCard creditCard) { await Task.Delay(1000); string message = $"Credit Card Number: {creditCard.CardNumber} Name: {creditCard.Name} Processed"; return message; } } public class CreditCard { public string CardNumber { get; set; } public string Name { get; set; } public static List<CreditCard> GenerateCreditCards(int number) { List<CreditCard> creditCards = new List<CreditCard>(); for (int i = 0; i < number; i++) { CreditCard card = new CreditCard() { CardNumber = "10000000" + i, Name = "CreditCard-" + i }; creditCards.Add(card); } return creditCards; } } }
Output:
Here, we have processed 100000 tasks simultaneously. But there might be problems when we execute a huge number of tasks simultaneously, for example, the server might be unable to handle such a huge request, or if we send 100000 HTTP requests to a server, it might be blocked or down.
So, instead of sending 100000 HTTP requests at a time or processing 100000 tasks simultaneously, what we need to do is, we need to send them as a batch or process the tasks as a batch and we can do this in C# by using SimaphoreSlim. With SemaphoreSlim, we are able to limit the number of concurrent tasks that will be executed with the Task.WhenAll method. Let us understand this with an example.
Example to Understand How to Limit the Number of Concurrent Tasks in C# using SemaphoreSlim:
For a better understanding, we are not going to process 100000 Credit Cards. What we will do is we will process 15 credit cards with a batch of 3. That means five batches will be executed to process the 15 credit cards. Let us see how we can achieve this.
First, we need to create an instance of SemaphoreSlim class as follows. Here, we are passing the initial capacity as 3. That means at a time 3 threads are allowed to execute the tasks.
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3);
So, here, what SemaphoreSlim is doing is if we have more than 3 tasks been running, we are going to wait and we’re going to wait until the semaphore gets released. If you are new to SimaphoreSlim, please read the following article where we discussed SimaphoreSlim in detail.
https://dotnettutorials.net/lesson/semaphoreslim-class-in-csharp/
Next, we need to convert the following piece of code of our ProcessCreditCards method to use SemaphoreSlim.
The following code shows how to use SimaphoreSlim to limit the number of concurrent tasks to be executed simultaneously. As we are using the WaitAsync method so, we need to use the async lambda expression, and also, we need to use await operator while calling the ProcessCard function. We need to release the semaphore inside the finally block which make sure that if an exception occurred then also the semapohoreslim object release the thread so that other task can be executed by the thread.
Complete Example Code:
The following is the complete example code that shows how to use SemaphoreSlim to limit the number of concurrent tasks. Here, it will execute the tasks in batches, and in each batch, it will execute a maximum of three tasks. In the below example, we need to include the System.Threading and System.Linq namespaces. SemaphoreSlim class belongs to System.Threading namespace and as we use LINQ queries so we need to include System.Linq namespace.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; namespace AsynchronousProgramming { class Program { //Allowing Maximum 3 tasks to be executed at a time static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3); static void Main(string[] args) { var stopwatch = new Stopwatch(); Console.WriteLine($"Main Thread Started"); //Generating 15 Credit Cards List<CreditCard> creditCards = CreditCard.GenerateCreditCards(15); Console.WriteLine($"Credit Card Generated : {creditCards.Count}"); ProcessCreditCards(creditCards); Console.WriteLine($"Main Thread Completed"); Console.ReadKey(); } public static async void ProcessCreditCards(List<CreditCard> creditCards) { var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task<string>>(); //Need to use async lambda expression tasks = creditCards.Select(async card => { //This will tell if we have more than 4000 tasks are running, //we are going to wait and ' //we're going to wait until the semaphore gets released. await semaphoreSlim.WaitAsync(); //Need to use await operator here as we are using asynchronous WaitAsync try { return await ProcessCard(card); } finally { //Release the semaphore semaphoreSlim.Release(); } }).ToList(); //It will execute a maximum of 3 tasks at a time await Task.WhenAll(tasks); stopwatch.Stop(); Console.WriteLine($"Processing of {creditCards.Count} Credit Cards Done in {stopwatch.ElapsedMilliseconds/1000.0} Seconds"); } public static async Task<string> ProcessCard(CreditCard creditCard) { await Task.Delay(1000); string message = $"Credit Card Number: {creditCard.CardNumber} Name: {creditCard.Name} Processed"; Console.WriteLine($"Credit Card Number: {creditCard.CardNumber} Processed"); return message; } } public class CreditCard { public string CardNumber { get; set; } public string Name { get; set; } public static List<CreditCard> GenerateCreditCards(int number) { List<CreditCard> creditCards = new List<CreditCard>(); for (int i = 0; i < number; i++) { CreditCard card = new CreditCard() { CardNumber = "10000000" + i, Name = "CreditCard-" + i }; creditCards.Add(card); } return creditCards; } } }
Output:
Here, please observe the output. It is taking a little more than 5 seconds and this is expected. Because it will execute all the tasks in five batches. And if you notice we have delayed the ProcessCard Execution by 1 second. That means executing one batch will take little more than 1 second and the same for all 5 batches, and hence the overall time is little more than 5 seconds.
How to Handle Response when Executing Multiple Tasks using Tasks.WhenAll Method in C#?
Now, let us understand how to handle the response when executing multiple tasks concurrently using Tasks.WhenAll Method in C#. We know Tasks.WhenAll tells that please wait for all of the tasks to be done before continuing with the execution of the rest part of the method. That means it will make sure once all the tasks are completed then only, we can proceed further to execute the rest part of the method.
If you further notice the return type of the ProcessCard card method is Task<string>. That means the method is returning something. As the WhenAll method execute all the tasks that means all the tasks return some data. How we can fetch such that? Let us see. Please have a look at the below image. If you put the mouse pointer over the await operator, then you will see that it is returning a string array.
So, we can store the response in a string array as follows:
string[] Responses= await Task.WhenAll(tasks);
Then by using a foreach loop we can access the result of each task as follows.
Complete Example Code:
Whatever we discussed is shown in the below example.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; namespace AsynchronousProgramming { class Program { //Allowing Maximum 3 tasks to be executed at a time static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3); static void Main(string[] args) { var stopwatch = new Stopwatch(); Console.WriteLine($"Main Thread Started"); //Generating 15 Credit Cards List<CreditCard> creditCards = CreditCard.GenerateCreditCards(15); Console.WriteLine($"Credit Card Generated : {creditCards.Count}"); ProcessCreditCards(creditCards); Console.WriteLine($"Main Thread Completed"); Console.ReadKey(); } public static async void ProcessCreditCards(List<CreditCard> creditCards) { var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task<string>>(); //Need to use async lambda expression tasks = creditCards.Select(async card => { await semaphoreSlim.WaitAsync(); try { return await ProcessCard(card); } finally { semaphoreSlim.Release(); } }).ToList(); string[] Responses = await Task.WhenAll(tasks); //var Responses = await Task.WhenAll(tasks); foreach (var response in Responses) { Console.WriteLine(response); } stopwatch.Stop(); Console.WriteLine($"Processing of {creditCards.Count} Credit Cards Done in {stopwatch.ElapsedMilliseconds / 1000.0} Seconds"); } public static async Task<string> ProcessCard(CreditCard creditCard) { await Task.Delay(1000); string message = $"Credit Card Number: {creditCard.CardNumber} Name: {creditCard.Name} Processed"; //Console.WriteLine($"Credit Card Number: {creditCard.CardNumber} Processed"); return message; } } public class CreditCard { public string CardNumber { get; set; } public string Name { get; set; } public static List<CreditCard> GenerateCreditCards(int number) { List<CreditCard> creditCards = new List<CreditCard>(); for (int i = 0; i < number; i++) { CreditCard card = new CreditCard() { CardNumber = "10000000" + i, Name = "CreditCard-" + i }; creditCards.Add(card); } return creditCards; } } }
Output:
Real-Time Example:
In the real-time application when we are calling an API, we will not get any string data. In most cases, we will get JSON data. If we are getting some JSON data then how we will handle it? We need to use JSON Serializer. To use JSON serializer, first, we need to include Newtonsoft.Json DLL from the NuGet. Once you install Newtonsoft.Json DLL from the NuGet, then modify the code as follows which shows how to use JSON serializer. The following example code is self-explained, so please go through the comment lines.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; using Newtonsoft.Json; namespace AsynchronousProgramming { class Program { //Allowing Maximum 3 tasks to be executed at a time static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3); static void Main(string[] args) { var stopwatch = new Stopwatch(); Console.WriteLine($"Main Thread Started"); //Generating 15 Credit Cards List<CreditCard> creditCards = CreditCard.GenerateCreditCards(15); Console.WriteLine($"Credit Card Generated : {creditCards.Count}"); ProcessCreditCards(creditCards); Console.WriteLine($"Main Thread Completed"); Console.ReadKey(); } public static async void ProcessCreditCards(List<CreditCard> creditCards) { var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task<string>>(); //Need to use async lambda expression tasks = creditCards.Select(async card => { await semaphoreSlim.WaitAsync(); try { return await ProcessCard(card); } finally { semaphoreSlim.Release(); } }).ToList(); //Return the response a string array var Responses = await Task.WhenAll(tasks); //Creating a collection to hold the responses List<CreditCardResponse> creditCardResponses = new List<CreditCardResponse>(); //Looping through the string array foreach (var response in Responses) { //Here, the string is a JSON string //Converting the JSON String to .NET Object (CreditCardResponse) using //JsonConvert class DeserializeObject CreditCardResponse creditCardResponse = JsonConvert.DeserializeObject<CreditCardResponse>(response); //Adding the .NET Object into the resposne collection creditCardResponses.Add(creditCardResponse); } //Printing all the approved credit cards using a foreach loop Console.WriteLine("\nApproved Credit Cards"); foreach(var item in creditCardResponses.Where(card => card.IsProcessed == true)) { Console.WriteLine($"Card Number: {item.CardNumber}, Name: {item.Name}"); } //Printing all the rejected credit cards using a foreach loop Console.WriteLine("\nRejected Credit Cards"); foreach (var item in creditCardResponses.Where(card => card.IsProcessed == false)) { Console.WriteLine($"Card Number: {item.CardNumber}, Name: {item.Name}"); } } public static async Task<string> ProcessCard(CreditCard creditCard) { await Task.Delay(1000); var creditCardResponse = new CreditCardResponse { CardNumber = creditCard.CardNumber, Name = creditCard.Name, //Logic to Decide whether the card is processed or rejected //If modulus 2 is 0, the processed else rejected IsProcessed = creditCard.CardNumber % 2 == 0 ? true : false }; //Converting the .NET Object to JSON string string jsonString = JsonConvert.SerializeObject(creditCardResponse); //Return the JSON String return jsonString; } } public class CreditCard { public long CardNumber { get; set; } public string Name { get; set; } public static List<CreditCard> GenerateCreditCards(int number) { List<CreditCard> creditCards = new List<CreditCard>(); for (int i = 0; i < number; i++) { CreditCard card = new CreditCard() { CardNumber = 10000000 + i, Name = "CreditCard-" + i }; creditCards.Add(card); } return creditCards; } } //This class will hold the response after processing the Credit card public class CreditCardResponse { public long CardNumber { get; set; } public string Name { get; set; } public bool IsProcessed { get; set; } } }
Output:
In the next article, I am going to discuss How to Cancel a Long-Running Task using Cancellation Token in C# with Examples. Here, in this article, I try to explain How to Limit the Number of Concurrent Tasks in C# using SemaphoreSlim with Examples. I hope you enjoy this How to Limit the Number of Concurrent Tasks in C# using SemaphoreSlim with Examples article.
Guys,
Please give your valuable feedback. And also, give your suggestions about this How to Limit the Number of Concurrent Tasks in C# concept. If you have any better examples, you can also put them in the comment section. If you have any key points related to How to Limit the Number of Concurrent Tasks in C#, you can also share the same.
Great article. To the point
I appreciate too much your excellent explanation of the articles, but I am faceing a big problem As I am a beginner C# programmer and I could not follow the steps of implementing the programs, for example in an article.
)How to Execute Multiple Tasks using the WhenAll Method in C# (
How to Limit Number of Concurrent Tasks in C# etc
I suggest “without using the arrows” to write the line number that must be after the current line that make tracking me step by step, this is helping to save time and efforts.
thanks
Task.Run() creates a thread from ThreadPool which is a background thread. so, without await the main thread does not wait for the background thread. As Tasks are an abstraction for Threads, is there a way to create a foreground thread using Task?