4

I have written a little winforms application that sends http requests to every ip address within my local network to discover a certain device of mine. On my particular subnet mask thats 512 addresses. I have written this using backGroundWorker but I wanted to tryout httpClient and the Async/Await pattern to achieve the same thing. The code below uses a single instance of httpClient and I wait until all the requests have completed. This issue is that the main thread gets blocked. I know this because I have a picturebox + loading gif and its not animating uniformly. I put the GetAsync method in a Task.Run as suggested here but that didn't work either.

    private async void button1_Click(object sender, EventArgs e)
    {
        var addresses = networkUtils.generateIPRange..
        await MakeMultipleHttpRequests(addresses);
    }


    public async Task MakeMultipleHttpRequests(IPAddress[] addresses)
    {
        List<Task<HttpResponseMessage>> httpTasks = new List<Task<HttpResponseMessage>>();
        foreach (var address in addresses)
        {
            Task<HttpResponseMessage> response = MakeHttpGetRequest(address.ToString());
            httpTasks.Add(response);
        }
        try
        {
            if (httpTasks.ToArray().Length != 0)
            {
                await Task.WhenAll(httpTasks.ToArray());
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("\thttp tasks did not complete Exception : {0}", ex.Message);
        }

    }

    private async Task<HttpResponseMessage> MakeHttpGetRequest(string address)
    {
        var url = string.Format("http://{0}/getStatus", address);
        var cts = new System.Threading.CancellationTokenSource();
        cts.CancelAfter(TimeSpan.FromSeconds(10));
        HttpResponseMessage response = null;
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        response = await httpClient.SendAsync(request, cts.Token);
        return response;
    }

I have read a similar issue here but my gui thread is not doing much. I have read here that I maybe running out of threads. Is this the issue, how can I resolve it? I know its the Send Async because if I replace the code with the simple task below there is no blocking.

    await Task.Run(() =>
    {
       Thread.Sleep(1000);
    });
CoderDam
  • 65
  • 6
  • I know what's happening. you have 500+ Http requests all throwing timeout exceptions at once.... Watch your "Output" window while it's locked up. – Andy Aug 20 '20 at 22:01
  • I think OP means there is some work done on the UI thread, and its interrupting the smooth animation of the gif. Yes just because you are using the async and await it actually doesn't imply threads at all, and continuations will run in the UI context. If you want to be assured everything is completely isolated from the ui, i suggest offloading. Task.Run( everything) – TheGeneral Aug 20 '20 at 22:02
  • Yes i do have timeout exceptions, i put a try a catch around the Send Async but shouldn't they be handled safely on their own threads? @JoePhillips blocking meaning , the loading spinner gif gets stuck, also i cannot click any ui buttons. – CoderDam Aug 20 '20 at 22:09
  • What I am saying: It's locking up because you are in Debug mode and the debugger is trying to say "hey, you got an exception" 500 times at once. Run it in Release mode and see if it still locks up. Also, you are doing timeouts wrong. – Andy Aug 20 '20 at 22:13
  • @Andy , yes that worked. The gif is not perfectly smooth but it's a lot better, I'm happy to accept this as an answer. Do you mean the setting up of the cancelation token is wrong? – CoderDam Aug 20 '20 at 22:27
  • OK -- added an answer. let me know if you need anything clarified. – Andy Aug 20 '20 at 22:40
  • What is your runtime? .NET Framework or .NET Core? – Paulo Morgado Aug 21 '20 at 07:47
  • @PauloMorgado .Net Framework 4.7.2 – CoderDam Aug 21 '20 at 08:03
  • `Task.WhenAll` takes an `IEnumerable` and, internally, creates an array of `Task`s. By invoking `ToArray()` you're forcing the creation of yet another array of `Task`s. – Paulo Morgado Aug 22 '20 at 19:47

2 Answers2

2

So one of the issues here is that you are creating 500+ tasks one after another in quick succession with a timeout set outside the task creation.

Just because you ask to run 500+ tasks, doesn't mean 500+ tasks are all going to run at the same time. They get queued up and run when the scheduler deems it's possible.

You set a timeout at the time of creation of 10 seconds. But they could sit in the scheduler for 10 seconds before they even get executed.

You want to have your Http requests to timeout organically, you can do that like this when you create the HttpClient:

private static readonly HttpClient _httpClient = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(10)
};

So, by moving the timeout to the HttpClient, your method should now look like this:

private static Task<HttpResponseMessage> MakeHttpGetRequest(string address)
{
    return _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, new UriBuilder
    {
        Host = address,
        Path = "getStatus"
    }.Uri));
}

Try using that method and see if it improves your lock-up issue in Debug mode.

As far as the issue you were having: It's locking up because you are in Debug mode and the debugger is trying to say "hey, you got an exception" 500 times all at the same time because they were all spawned at the same time. Run it in Release mode and see if it still locks up.

What I would consider doing is batching out your operations. Do 20, then wait until those 20 finish, do 20 more, so on and so forth.

If you'd like to see a slick way of batching tasks, let me know and I would be more than happy to show you.

Andy
  • 12,859
  • 5
  • 41
  • 56
  • 1
    Using a SemaphoreSlim to throttle the requests would likely be the best way to go about this – Joe Phillips Aug 20 '20 at 23:40
  • @JoePhillips -- *absolutely*, if he wants to see an example, that's exactly what i would have used. Semaphore with like 20 or so slots. – Andy Aug 20 '20 at 23:42
  • Or a TPL ActionBlock. – Paulo Morgado Aug 21 '20 at 07:47
  • @Andy, thanks, yes in release mode , everything works smoothly. I tried setting the httpClient timeout as you suggested but it did not make a difference in debug mode, I guess it's because 99% of the calls will always end with an exception. Please do give an example of doing batch operations, thats very interesting. Marking this as the answer. – CoderDam Aug 21 '20 at 07:53
1

On .NET Framework, the number of connections to a server is controlled by the ServicePointManager Class.

For a client, the default connection limit is 2 on client processes.

No matter how many HttpClient.SendAsync invocations you do, only 2 will be active at the same time.

But you can manage the connections yourself.

On .NET Core here isn't the concept of service point manager and the equivalent default limit is int.MaxValue.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • This looks interesting. Do you have more information on when/how this would be useful? Perhaps an example? – Joe Phillips Aug 21 '20 at 13:50
  • On ASP.NET server applications this number will be higher because it's more likely to be more simultaneous requests to the same host/URL. On client applications it's useful when you need more than 2 connections to the same host/URL. The system is very configurable. Please, look at the docs in the provided links and in the links on those docs. – Paulo Morgado Aug 21 '20 at 13:54
  • Thats very interesting read, thanks. In my case i'm making a single call to multiple hosts so I don't think i am reaching the limit as described. I did try it but it made no difference. However i'll remember to set it to more than 2 when my application does do that. – CoderDam Aug 21 '20 at 15:37
  • Have you verified that the connections are, actually, being made? Either using `netsat` or Fiddler? – Paulo Morgado Aug 21 '20 at 15:41
  • and you see all those 512 connections to the servers at the same time? – Paulo Morgado Aug 21 '20 at 18:00
  • no not the same time and didn't see a difference by increasing defaultconnectionlimit – CoderDam Aug 22 '20 at 07:24
  • milliseconds between each call – CoderDam Aug 22 '20 at 07:38
  • If the invocations are for different hosts, changing that wouldn't make a difference. – Paulo Morgado Aug 22 '20 at 19:44
  • You need to capture a trace of the execution with [PerfView](https://github.com/Microsoft/perfview) to see what's happening. – Paulo Morgado Aug 22 '20 at 19:45