【问题标题】:C# Slow UI Performance when calling BeginInvoke frequently频繁调用 BeginInvoke 时 C# 缓慢的 UI 性能
【发布时间】:2018-04-29 07:26:30
【问题描述】:

我有一个名为 ProxyTesterForm 的主窗体,它有一个子窗体 ProxyScraperForm。当 ProxyScraperForm 抓取新的代理时,ProxyTesterForm 通过异步测试抓取的代理来处理事件,并在测试后将代理添加到作为 DataGridView 的数据源的 BindingList。

因为我要添加到在 UI 线程上创建的数据绑定列表,所以我在 DataGridView 上调用 BeginInvoke,因此更新发生在适当的线程上。

在我将在下面发布的方法中没有 BeginInvoke 调用,我可以在处理过程中在屏幕上拖动表单,它不会卡顿并且很流畅。对于 BeginInvoke 调用,它的作用正好相反。

我对如何解决它有一些想法,但想在 SO 上听到比我更聪明的人的意见,所以我妥善解决了这个问题。

  1. 使用信号量 slim 来控制同时更新的数量。

  2. 将异步处理的项目添加到我将在下面发布的方法范围之外的列表中,并在 Timer_Tick 事件处理程序中迭代该列表,每 1 秒为列表中的每个项目调用 BeginInvoke,然后清除该列表并清洗,冲洗,重复直到工作完成。

  3. 放弃数据绑定的便利,转为虚拟模式。

  4. 其他人可能会在这里提出建议。

    private void Site_ProxyScraped(object sender, Proxy proxy)
    {
        Task.Run(async () =>
        {
            proxy.IsValid = await proxy.TestValidityAsync(judges[0]);
            proxiesDataGridView.BeginInvoke(new Action(() => { proxies.Add(proxy); }));
        });
    }
    

【问题讨论】:

  • 您是否考虑过使用取消令牌来防止多个同时请求?
  • @Stefan 不,我没有。这不是信号量的用途吗?我只使用了取消令牌来取消任务。
  • 老实说,我认为您在这里不需要Task.Run。而是将Site_ProxyScraped 设为异步,因为我认为这是一个事件处理程序。然后 await proxy.TestValidtiyAsync 和 BeginInvoke 可以保持原样。
  • 考虑分层:它将帮助您克服这里的许多问题。例如,请参阅此(我自己的 XD)帖子; stackoverflow.com/questions/23648832/…
  • 关键是将proxy.IsValid = await proxy.TestValidityAsync(judges[0]);的结果存储在DictionaryCollection中。它将非常快,并且不需要与 UI 交互。稍后您可以考虑更新 UI。也许有一个间隔为 500 毫秒的计时器或类似的东西。您将从 DictionaryCollection 更新 UI

标签: c#


【解决方案1】:

在 Windows 中,每个具有 UI 的线程都有一个消息队列 - 该队列用于为该线程的窗口发送 UI 消息,这些消息包括鼠标移动、鼠标向上/向下等内容。

在每个 UI 框架中的某个地方都有一个循环,它从队列中读取消息,对其进行处理,然后等待下一条消息。

有些消息的优先级较低,例如鼠标移动消息只有在线程准备好处理它时才会生成(因为鼠标往往会移动很多)

BeginInvoke 也使用这种机制,它发送一条消息告诉循环有代码需要运行。

您正在做的是用您的 BeginInvoke 消息淹没队列,而不是让它处理 UI 事件。

标准的解决方案是限制 BeginInvoke 调用的数量,例如,收集您需要添加的所有项目,并使用一个 BeginInvoke 调用将它们全部添加。

或者批量添加,如果您每秒只对这一秒内找到的所有对象进行一次 BeginInvoke 调用,您可能不会影响 UI 响应性并且用户将无法区分。

【讨论】:

  • 感谢您的出色解释。我感觉是这样的,你提出的解决方案和我想的差不多,但想和比我更了解的人确认一下。
【解决方案2】:

注意:有关为什么会发生这种情况的实际答案,请参阅@Nir 的答案。这只是为了克服一些问题并给出一些方向的解释。它并非完美无缺,但它符合 cmets 的对话。

只是一些快速的原型类型来添加一些层的分离(最小的尝试):

//member field which contains all the actual data
List<Proxy> _proxies = new List<Proxy>();

//this is some trigger: it might be an ellapsed event of a timer or something
private void OnSomeTimerOrOtherTrigger()
{ 
      UIupdate();
}

//just a helper function
private void UIupdate
{
    var local = _proxies.ToList(); //ensure static encapsulation 
    proxiesDataGridView.BeginInvoke(new Action(() => 
    {    
         //someway to add *new ones* to UI
         //perform actions on local copy
    }));
}

private void Site_ProxyScraped(object sender, Proxy proxy)
{
    Task.Run(async () =>
    {
        proxy.IsValid = await proxy.TestValidityAsync(judges[0]);
        //add to list
        _proxies.Add(proxy);
    });
}

【讨论】:

  • 我会将 Site_ProxyScraped 转换为异步。这里不需要Task.Run
  • @Stefan 谢谢,我很快就会尝试一下。如果我采用此解决方案,我会将其标记为答案。
  • @FCin 从现在开始,我将使用需要等待调用的事件处理程序来执行此操作。
  • @FCin:是的,这是一个很好的建议,尽管我不是调用该函数的对象,并且如果 OP 能够处理调用者不异步的情况。
  • @DavidStampher: ensure static encapsulation;这是我的俚语:延迟/延迟执行和封闭。想象一下,UI 正在从_proxies 的列表中更新,并且该列表在其他地方发生了更改,这可能是因为 UI 相对较慢。同时从 2 个不同的地方访问一个列表可能会导致问题:即访问可能被删除的元素。所以:处理本地副本将克服这个问题。这就是那里发生的事情。我添加了一条评论:“对本地副本执行操作”。
猜你喜欢
  • 1970-01-01
  • 2018-04-28
  • 1970-01-01
  • 1970-01-01
  • 2012-09-23
  • 1970-01-01
  • 2011-03-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多