c#异步方法仍然挂起UI

本文关键字:挂起 UI 异步方法 | 更新日期: 2023-09-27 18:02:15

我有这两个方法,我想异步运行以保持UI响应。但是,它仍然挂着UI。有什么建议吗?

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;
        }
    }

c#异步方法仍然挂起UI

我怀疑DownloadStringTaskAsync在较低水平上依赖于HttpWebRequest.BeginGetResponse。在这种情况下,我们知道webrequest的设置并不是完全异步的。令人恼火的是(坦率地说,愚蠢地),异步WebRequest的DNS查找阶段是同步执行的,因此阻塞。我怀疑这可能是你正在观察的问题。

下面复制的是文档中的警告:

BeginGetResponse方法需要完成一些同步设置任务(例如,DNS解析、代理检测和TCP套接字连接)才能使该方法变为异步。因此,永远不应该在用户界面(UI)线程上调用此方法,因为它可能需要一些时间,通常是几秒钟。在某些没有正确配置webproxy脚本的环境中,这可能需要60秒或更长时间。配置文件元素上的downloadTime属性的默认值是一分钟,这占了大部分潜在的时间延迟。

你有两个选择:

  1. 从工作线程启动请求(并且在高负载下,由于阻塞行为而冒线程池耗尽的风险)
  2. (tenulous)在触发请求之前执行编程式DNS查找。这个可以异步完成。希望请求能够使用缓存的DNS查找。

我们选择了第三种(也是昂贵的)实现我们自己的适当异步HTTP库来获得体面的吞吐量,但对于您的情况可能有点极端;)

你似乎把异步和并行搞混了。它们都基于任务,但它们是完全不同的。不要假设async方法是并行运行的——它们不是。

Async默认在相同的线程中工作,除非有原因迫使异步引擎启动一个新线程,例如主线程没有消息泵的情况。但是一般来说,我倾向于认为async关键字在同一个线程中运行。

你使用WinForms,所以UI线程有一个消息泵。因此,上面所有的代码都在UI线程中运行。

您必须明白,这里NOT引入了任何并行性。您通过async关键字引入的是异步操作,而不是 parallel。你没有做任何事情来"使你的UI响应",除了一个调用DownloadStringTaskAsync不会强迫你等待数据到达,但是你仍然必须在UI线程中做所有的网络处理(DNS查找等)——这里是异步操作(你基本上"节省"等待下载的时间)。

为了保持UI的响应性,你需要将耗时的工作分离到一个单独的线程中,同时保持UI线程空闲。您不能对async关键字这样做。

你需要使用Task.Factory.StartNew(...)来显式地启动一个新线程来做你的后台处理。

您要向列表视图中添加多少项?

除非你采取措施防止它,WinForms列表视图将做一个批次处理每次你添加一个项目到列表中。这可能需要很长时间,添加100项可能需要几秒钟。

尝试在你的循环中使用BeginUpdateEndUpdate来推迟ListView的记账,直到你完成。

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();
    }
}

最后必须使用try来避免出现异常时的各种痛苦