DEV Community

Cover image for Concurrent HTTP Call Using SemaphoreSlim in .NET
Bervianto Leo Pratama
Bervianto Leo Pratama

Posted on

Concurrent HTTP Call Using SemaphoreSlim in .NET

Introduction

I'm exploring concurrent HTTP calls and learning how to handle many tasks in .NET. I found some references about SemaphoreSlim to handle concurrent tasks. If you want to know more about SemaphoreSlim, please see this reference.

Preparing the Web API

I just use the Web API from the template. You can use this command to generate the project.

dotnet new webapi -o WebServer
Enter fullscreen mode Exit fullscreen mode

Don't forget to run the Web API using this command: dotnet run --project WebServer.

Generating the Console App to Call the Web API

You can use this command to generate the console app.

dotnet new console -o WebServer.Client
Enter fullscreen mode Exit fullscreen mode

My Learning Process

I found many patterns and suggestions to use this code.

// just for generate 10,000 data
var result = Enumerable.Range(0, 10_000);
// generate tasks
var tasks = result.Select(async x => {
   // API Call
  await CallServer();
});
await Task.WhenAll(tasks);
Enter fullscreen mode Exit fullscreen mode

But the code introduces a big problem. Some tasks will reach the request timeout because they exceed the concurrent limit to create connections and can't do API calls. The queue happens in HttpClient.

Thank you, Josef Ottosson. This post inspires me so much. I've tried to use SemaphoreSlim, and that is my expectation to do concurrent calls.

Here are my final codes.

using System.Collections.Concurrent;
using System.Diagnostics;

var stopwatch = new Stopwatch();
using HttpClient client = new();

var semaphoreSlim = new SemaphoreSlim(initialCount: 10,
          maxCount: 10);
Console.WriteLine("{0} tasks can enter the semaphore.",
                          semaphoreSlim.CurrentCount);
var result = Enumerable.Range(0, 20_000);
var dictionaryResult = new ConcurrentBag<string>();
stopwatch.Start();
var tasks = result.Select(async x =>
{
    Console.WriteLine("Task {0} begins and waits for the semaphore.",
                                  x);
    int semaphoreCount;
    await semaphoreSlim.WaitAsync();
    try
    {
        Console.WriteLine("Task {0} enters the semaphore.", x);
        var response = await GetForecastAsync(x, client);
        dictionaryResult.Add(response);
    }
    finally
    {
        semaphoreCount = semaphoreSlim.Release();
    }
    Console.WriteLine("Task {0} releases the semaphore; previous count: {1}.",
                                  x, semaphoreCount);
});
Console.WriteLine("Waiting task");
await Task.WhenAll(tasks);
stopwatch.Stop();
Console.WriteLine(dictionaryResult.Count);
// Get the elapsed time as a TimeSpan value.
TimeSpan ts = stopwatch.Elapsed;

// Format and display the TimeSpan value.
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
    ts.Hours, ts.Minutes, ts.Seconds,
    ts.Milliseconds / 10);
Console.WriteLine("RunTime " + elapsedTime);

static async Task<string> GetForecastAsync(int i, HttpClient client)
{
    Console.WriteLine($"Request from: {i}");
    var response = await client.GetAsync("http://localhost:5153/weatherforecast");
    return await response.Content.ReadAsStringAsync();
}
Enter fullscreen mode Exit fullscreen mode

We move the "queue" process to SemaphoreSlim. I think this will be the best approach if you don't need to have guaranteed order. How about having guaranteed order results?

I have a "hacky" way, but this is not the best approach. You can use the ConcurrentDictionary, but you will need to sort the ConcurrentDictionary using the index. Please see these code changes.

// set to ConcurrentDictionary<int, string>
var dictionaryResult = new ConcurrentDictionary<int, string>();

// ...

// store response, x is the index of the task lists
dictionaryResult.TryAdd(x, response);

// sort by the key and just take the response
var responses = dictionaryResult.OrderBy(x => x.Key).Select(x => x.Value).ToList();
Enter fullscreen mode Exit fullscreen mode

This approach might become slow because it's run sequentially. Feel free to give feedback on this post if you have another suggestion that runs concurrently or parallels.

You can access the code here.

GitHub logo berviantoleo / HttpClientConcurrent

Exploring HttpClient Concurrent

Thank you

Thank you for reading this post.

GIF Thank You

Resources/References

Top comments (2)

Collapse
 
gwigwam profile image
GWigWam

HttpClient has build-in features for managing concurrent requests:

ServicePointManager.FindServicePoint(new Uri(url)).ConnectionLimit = maxConcurrentRequests;
Enter fullscreen mode Exit fullscreen mode

And you can avoid timeouts by changing the HttpClient.Timeout property. Probably a lot easier than using a semaphore in most cases.

Collapse
 
berviantoleo profile image
Bervianto Leo Pratama

Agreed. In my case, I want to avoid increasing the timeout, but your suggestion will be a good alternative.