2

I'm writing an app in which I have to scan an ip (intranet) range and see if the particular ip corresponds to a specific url. For example let's say we have the url: http://<ip>:8080/x/y and we want to see if we can find an active server running in the range 192.168.1.1 - 192.168.1.254. Obviously, the scan process should not block the UI.

So I created the following async method:

    private List<AvailableServer> _availableServerList;

    public List<AvailableServer> AvailableServerList
    {
        get { return _availableServerList;}
        set { _availableServerList = value;}
    }

    private Mutex objLock = new Mutex(true, "MutexForScanningServers");
    private long IPsChecked;
    private List<string> allPossibleIPs;

    CancellationTokenSource cts;

    public async Task GetAvailableServers()
    {
        Dictionary<string, string> localIPs = LocalIPAddress(); //new Dictionary<string, string>();

        allPossibleIPs = new List<string>();

        Dictionary<string, CancellationToken> kvrList = new Dictionary<string, CancellationToken>();
        cts = new CancellationTokenSource();

        foreach (KeyValuePair<string, string> kvp in localIPs)
        {
            allPossibleIPs.AddRange(getIpRange(kvp.Key.ToString(), kvp.Value.ToString()));
        }

        foreach (string ip in allPossibleIPs)
        {
            kvrList.Add(ip, cts.Token);
        }

        AvailableServerList = new List<AvailableServer>();

        var downloads = kvrList.Select(kvr => isServerAvailableAsync(kvr));
        Task[] dTasks = downloads.ToArray();
        await Task.WhenAll(dTasks).ConfigureAwait(false);
    }

whose purpose is to start a banch of Tasks, each one of them trying to receive a valid HttpClient request. Let's say the request is valid if the HttpClient.GetStringAsync does not throw any exception:

    private async Task isServerAvailableAsync(Object obj)
    {
        using (var client = new HttpClient())
        {
            try
            {
                KeyValuePair<string, CancellationToken> kvrObj = (KeyValuePair<string, CancellationToken>)obj;
                string urlToCheck = @"http://" + kvrObj.Key + ":8080/x/y";

                string downloadTask = await client.GetStringAsync(urlToCheck).ConfigureAwait(false);

                string serverHostName = Dns.GetHostEntry(kvrObj.Key).HostName;
                AvailableServerList.Add(new AvailableServer(serverHostName, @"http://" + serverHostName + ":8080/x/y"));
               // }
           }
            catch (System.Exception ex)
            {
                //...Do nothing for now...
            }
            finally
            {
                lock (objLock)
                {
                    //kvrObj.Value.ThrowIfCancellationRequested();
                    IPsChecked++;
                    int tmpPercentage = (int)((IPsChecked * 100) / allPossibleIPs.Count);
                    if (IPsCheckedPercentageCompleted < tmpPercentage)
                    {
                        IPsCheckedPercentageCompleted = tmpPercentage;
                        OnScanAvailableServersStatusChanged(new EventArgs());
                    }
                }
            }
        }
    }

If the request is valid then we found an available server and so we add the url to our list. Else we catch an exception. Finally we update our percentage variables, because we want to pass them to our UI (xx% scanned). Each time a Task completes it fires a delegate which is used by our UI to get the new updated list of available servers and percentage completed. The main async function GetAvailableServers starts running via Task.Run(() => className.GetAvailableServers()) which exists inside a DelegateCommand which resides into a ViewModel of my own:

    public ICommand GetAvailableServersFromViewModel
    {
        get
        {
            return new DelegateCommand
            {
                CanExecuteFunc = () => true,
                CommandAction = () =>
                {
                    Task.Run(() => utilsIp.GetAvailableServers());
                }
            };
        }
    }

The problem with my implementation is that the UI lags while scanning, which is easily seen through the loading spinner I have in my UI. Well the code is far from best and I know I am wrong somewhere.

svick
  • 236,525
  • 50
  • 385
  • 514
jded
  • 121
  • 5
  • 2
    Pause the debugger 10 times under maximum load. Review the UI thread stack each time. What code is running? That's the code that's lagging your UI. – usr Feb 09 '15 at 22:51
  • I tried that but I did not find something clear to me. I also tried to remove any UI logic related to the percentage updates and I left the code only with the Task.Run(() => utilsIp.GetAvailableServers()); Again I got lag. It seems like the problem is when await client.GetStringAsync throws an exception, (I have tons of exceptions cause 99% of the IPs in the scan process do not exist at all), but I am not sure why I have this behavior as this is supposed to work on a different Thread or so.... – jded Feb 15 '15 at 16:03
  • Change the URL to always point to a valid endpoint. Are the lags gone? If yes the errors have to do with the problem. – usr Feb 15 '15 at 19:38
  • I tested this scenario. At the beginning I had the impression that the lags were gone. Then I tried to maximize the Tasks with valid URL address to 5000. Then again the same problem in UI and this time without any exceptions. I 'll continue the research... – jded Feb 17 '15 at 22:20
  • 1
    "I did not find something clear to me" Maybe I can help with that. If the UI is lagging code *must* be running on the UI thread. Post some stacks and I'll take a look. – usr Feb 18 '15 at 09:55

1 Answers1

0

As you know (based on your code), when you await a task on the UI thread, then the captured context for the task is the UI Context, which always runs on a single thread.

This means that when your task is complete, then control comes back in on the UI thread and then runs the continuation code on that thread.

You can try to avoid this by using the .ConfigureAwait(false) method (which you are). That call turns off the configuring of the task's thread's synchronization context, so when the continuation code is ready to run it will use the default context, which means it will be executed on the thread pool instead of the UI thread where the task originated from.

However, as described here (and hinted at here), if the task completes before it is awaited, then no thread switching is done and the continuation code will be executed on the UI thread.

This is where I think your problem is, since the code following the await code can sometimes run on the UI thread. The code right after the await is Dns.GetHostEntry(...) and "GetHostEntry is ridiculously slow, particularly when no reverse DNS entry is found".

We can see from here and here that "the GetHostEntry method queries a DNS server for the IP address that is associated with a host name or IP address", which means network I/O, which means blocking call, which means laggy UI.

That is a long way of saying that I believe that a solution to the issue is to also have GetHostEntry(...) wrapped in a task, just in case the first await doesn't block and it ends up being run on the UI thread.

Community
  • 1
  • 1
Matt Klein
  • 7,856
  • 6
  • 45
  • 46