C# 异步方法仍然挂起 UI

发布于 2024-11-24 18:23:29 字数 1490 浏览 3 评论 0原文

我有这两种方法,我想运行异步以保持 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;
        }
    }

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

み青杉依旧 2024-12-01 18:23:29

我怀疑 DownloadStringTaskAsync 依赖于 < code>HttpWebRequest.BeginGetResponse 在较低级别。在这种情况下,我们知道 Web 请求的设置并不是完全异步的。令人烦恼(坦率地说,愚蠢的是)异步 WebRequest 的 DNS 查找阶段是同步执行的,因此会阻塞。我怀疑这可能是您正在观察的问题。

下面转载的是文档中的警告:

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

您有两种选择:

  1. 从工作线程启动请求(在高负载下,运行由于阻塞行为而导致 ThreadPool 饥饿的风险)
  2. (谨慎地)在触发请求之前执行编程式 DNS 查找。此可以异步完成。希望该请求随后将使用缓存的 DNS 查找。

我们选择了第三种(也是昂贵的)选项,即实现我们自己的正确异步 HTTP 库以获得不错的吞吐量,但在您的情况下这可能有点极端;)

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

笑红尘 2024-12-01 18:23:29

您似乎将异步与并行混淆了。它们都是基于Tasks,但又完全不同。不要假设async 方法是并行运行的——它们不是并行运行的。

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

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

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

为了保持 UI 的响应能力,您需要将耗时的工作分拆到单独的线程中,同时保持 UI 线程空闲。您没有使用 async 关键字来执行此操作。

您需要使用 Task.Factory.StartNew(...) 显式启动一个新线程来执行后台处理。

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.

九歌凝 2024-12-01 18:23:29

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

除非您采取措施阻止它,否则每次您向列表中添加项目时,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 finally 来避免各种痛苦。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文