C# async methods still hang UI
Asked Answered
D

3

10

I have these two methods, that I want to run async to keep the UI responsive. However, it's still hanging the UI. Any suggestions?

async void DoScrape()
    {
        var feed = new Feed();

        var results = await feed.GetList();
        foreach (var itemObject in results)
        {
            var item = new ListViewItem(itemObject.Title);
            item.SubItems.Add(itemObject.Link);
            item.SubItems.Add(itemObject.Description);
            LstResults.Items.Add(item);
        }
    }


    public class Feed
    {
        async public Task<List<ItemObject>> GetList()
        {
            var client = new WebClient();
            string content = await client.DownloadStringTaskAsync(new Uri("anyUrl"));
            var lstItemObjects = new List<ItemObject>();
            var feed = new XmlDocument();
            feed.LoadXml(content);
            var nodes = feed.GetElementsByTagName("item");

            foreach (XmlNode node in nodes)
            {
                var tmpItemObject = new ItemObject();
                var title = node["title"];
                if (title != null) tmpItemObject.Title = title.InnerText;
                var link = node["link"];
                if (link != null) tmpItemObject.Link = link.InnerText;
                var description = node["description"];
                if (description != null) tmpItemObject.Description = description.InnerText;
                lstItemObjects.Add(tmpItemObject);
            }
            return lstItemObjects;
        }
    }
Dissonant answered 17/7, 2011 at 19:14 Comment(10)
And how are you calling your async methods? I suspect you call them and then issue access their result in a blocking way.Straiten
Are you using the background thread? What are you using... WPF / Silverlight / WinForms?Hindman
WinForms. No bg threads. Imagine that the only code in the project and DoScrape() being called from a button click.Dissonant
hmm I see nothing wrong with this code. The parsing is obviously still synchronous, but that's most likely cheap.Straiten
see #4058426Seaden
Use a Stopwatch to measure how much time parsing the xml takes. And how long it takes to fill the list view.Hexane
When the UI is blocked, how does the stack trace of out main thread look like and what line is it currently executing?Mycorrhiza
@jwJung why should it be in a background thread? These functions are intended to run on the main thread with the sole exception of the download itself.Straiten
@CodeInChaos: Yes you're right. I misunderstood it, so I deleted my comment.Baldachin
Can you break in the debugger when the UI is blocking to see where it is blocking? Or perhaps comment out everything except the downloads. So we can know for sure if it's the download not being async enough, or if it's something else.Straiten
F
13

I suspect DownloadStringTaskAsync relies upon HttpWebRequest.BeginGetResponse at a lower level. This being the case, it is known that the setup for a webrequest is not fully asynchronous. Annoyingly (and frankly, stupidly) the DNS lookup phase of an asynchronous WebRequest is performed synchronously, and therefore blocks. I suspect this might be the issue you are observing.

Reproduced below is a warning in the docs:

The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. As a result, this method should never be called on a user interface (UI) thread because it might take some time, typically several seconds. In some environments where the webproxy scripts are not configured properly, this can take 60 seconds or more. The default value for the downloadTime attribute on the config file element is one minute which accounts for most of the potential time delay.

You've two choices:

  1. Start the request from a worker thread (and under high load, run the risk of ThreadPool starvation due to blocking behaviour)
  2. (Tenuously) Perform a programmatic DNS lookup prior to firing the request. This can be done asynchronously. Hopefully the request will then use the cached DNS lookup.

We went for the 3rd (and costly) option of implementing our own properly asynchronous HTTP library to get decent throughput, but it's probably a bit extreme in your case ;)

Forgot answered 17/7, 2011 at 19:47 Comment(1)
seems like a nice explanation. but it didn't help in my case.I moved the HttpClient.GetByteArrayAsync call from an async function on UI thread to a threadpool worker.Pancreatotomy
S
5

You seem to be confusing async with parallel. They are both based on Tasks, but they are completely different. Do not assume that async methods run in parallel -- they don't.

Async defaults to work in the same thread, unless there are reasons that force the async engine to spin up a new thread, such as the case when the main thread does not have a message pump. But in general, I tend to think of the async keyword as running in the same thread.

You use WinForms, so the UI thread has a message pump. Therefore, all your code above runs in the UI thread.

You must understand that you have NOT introduced any parallelism here. What you have introduced via the async keyword is asynchronous operations, NOT parallel. You have not done anything to "make your UI responsive" except for that one call to DownloadStringTaskAsync which won't force you to wait for the data to arrive, but you STILL have to do all the network processing (DNS lookup etc.) in the UI thread -- here is the asynchronous operation in play (you essentially "save" the time waiting for downloads).

In order to keep UI's responsive, you need to spin off time-consuming work into a separate thread while keeping the UI thread free. You're not doing this with the async keyword.

You need to use Task.Factory.StartNew(...) to explicitly spin up a new thread to do your background processing.

Surfeit answered 18/7, 2011 at 2:2 Comment(2)
But it looks to me that downloading is the only time consuming operation in his code. A bit of xml parsing and UI updating is most likely cheap. @spender's explanation sounds more probable.Straiten
@CodeInChaos, true. However, I am just trying to make the point about async being NOT parallel. Therefore, anything done with "await" is actually done in the UI thread. That is INCLUDING the DNS lookup and other network access. Which means that only the wait-for-data-to-arrive time is saved with async code, not anything else. I'll edit my answer to be more specific.Surfeit
E
4

How many items are you adding into your list view?

Unless you take action to prevent it, the WinForms list view will do a lot of processing every time you add an item into the list. This can take so long that adding just 100 items can take several seconds.

Try using BeginUpdate and EndUpdate around your loop to defer the bookkeeping of ListView until you're finished.

async void DoScrape()
{
    var feed = new Feed();

    var results = await feed.GetList();
    LstResults.BeginUpdate();  // Add this
    try
    {
        foreach (var itemObject in results)
        {
            var item = new ListViewItem(itemObject.Title);
            item.SubItems.Add(itemObject.Link);
            item.SubItems.Add(itemObject.Description);
            LstResults.Items.Add(item);
        }
    }
    finally
    {
        LstResults.EndUpdate();
    }
}

Got to use a try finally to avoid all sorts of pain if there's an exception.

Earflap answered 17/7, 2011 at 19:52 Comment(1)
That didn't do the trick. I was following this guide msdn.microsoft.com/en-us/vstudio/gg440604 and my methods are not so different from this guide. I just don't get it.Dissonant

© 2022 - 2024 — McMap. All rights reserved.