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
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
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);
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();
}
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();
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.
berviantoleo / HttpClientConcurrent
Exploring HttpClient Concurrent
HttpClientConcurrent
Exploring HttpClient Concurrent
Thank you
Thank you for reading this post.
Top comments (2)
HttpClient has build-in features for managing concurrent requests:
And you can avoid timeouts by changing the
HttpClient.Timeout
property. Probably a lot easier than using a semaphore in most cases.Agreed. In my case, I want to avoid increasing the timeout, but your suggestion will be a good alternative.