C#在频繁调用BeginInvoke时UI表现缓慢。

4
我有一个名为ProxyTesterForm的主表单,它有一个子表单ProxyScraperForm。当ProxyScraperForm抓取新代理时,ProxyTesterForm通过异步测试处理事件,并在测试后将代理添加到绑定列表中,该列表是DataGridView的数据源。
因为我正在添加到在UI线程上创建的数据绑定列表中,所以我正在对DataGridView调用BeginInvoke,以便更新发生在适当的线程上。
如果在下面发布的方法中没有BeginInvoke调用,我可以在处理过程中在屏幕上拖动表单而不会卡顿并且很流畅。调用BeginInvoke后,它则相反。
我有一些想法来解决这个问题,但想听听比我更聪明的人在SO上的意见,以便我能正确解决它。
  1. Use a semaphore slim to control the amount of simultaneous updates.

  2. Add asynchronously processed items to a list outside of the scope of the the method I will post below, and iterate over that list in a Timer_Tick event handler, calling BeginInvoke for each item in the list every 1 second, then clearing that list and wash, rinse, repeat until the job is done.

  3. Give up the convenience of data binding and go virtual mode.

  4. Anything else someone might suggest here.

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

说实话,在这里我认为你不需要使用 Task.Run。相反,将 Site_ProxyScraped 改为异步,因为我假设这是一个事件处理程序。然后等待 proxy.TestValidtiyAsync,而 BeginInvoke 可以保持不变。 - FCin
@FCin 我听说要避免使用异步 void 方法签名。我不确定运行任务是否导致了我遇到的性能问题,但我可以尝试一下。 - JohnWick
@Stefan 好的,这是我以前没有做过的事情。在我的情况下,实现起来很简单吗? - JohnWick
1
考虑分层设计:这将帮助您克服许多问题。请参考我的博客,了解示例:https://dev59.com/m2Ag5IYBdhLWcg3wYqJ0#23649191 - Stefan
1
关键是将 proxy.IsValid = await proxy.TestValidityAsync(judges[0]); 的结果存储在一个 DictionaryCollection 中。这样会非常快速,而且不需要与 UI 进行交互。然后您可以考虑更新 UI。也许使用 500ms 的时间间隔定时器之类的东西。您可以从 DictionaryCollection 更新 UI。 - Stefan
显示剩余16条评论
2个回答

3
在Windows中,每个具有UI的线程都有一个消息队列——这个队列用于向此线程的窗口发送UI消息,这些消息包括鼠标移动、鼠标上下等。
在每个UI框架中都有一个循环,用于从队列中读取消息、处理它并等待下一个消息。
有些消息优先级较低,例如,只有当线程准备好处理它时,才会生成鼠标移动消息(因为鼠标移动非常频繁)。
BeginInvoke也使用了这种机制,它发送一条消息告诉循环需要运行的代码。
你正在用BeginInvoke消息淹没队列,并且不让它处理UI事件。
标准解决方案是限制BeginInvoke调用的数量,例如,收集所有需要添加的项,并使用一个BeginInvoke调用将它们全部添加。
或者分批添加,如果您每秒只进行一次BeginInvoke调用来查找此秒钟中找到的所有对象,则可能不会影响UI的响应性,用户无法感知差异。

感谢您的出色解释。我有一种感觉,这就是情况,并且您建议的解决方案基本上也是我所考虑的,但我想确认一下比我更懂的人。 - JohnWick

1
注意:关于为什么会发生这种情况的实际答案,请参见@Nir的答案。这只是一个解释,以克服一些问题并提供一些方向。它并不完美,但它符合评论中的对话线路。
只是一些快速原型,添加了一些层次分离(最小尝试):
//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);
    });
}

1
我会将 Site_ProxyScraped 转换为异步的。这里不需要使用 Task.Run - FCin
@Stefan 谢谢,我很快就会尝试这个。如果我选择这个解决方案,我会将其标记为答案。 - JohnWick
@FCin 从现在开始,我会使用需要等待调用的事件处理程序来完成这个任务。 - JohnWick
@FCin:是的,那是个好建议,尽管我不知道是谁在调用那个函数,如果 OP 能够处理调用者不是异步的情况就更好了。 - Stefan
@Stefan 请问您在创建 _proxies 的本地副本时所说的“确保静态封装”,是什么意思?此外,我在将项目添加到datagridview数据源后,是否应调用_proxies.Clear()呢? - JohnWick
1
@DavidStampher:通过“确保静态封装”,这是我的俚语,意思是延迟执行和封闭。想象一下,当UI从“_proxies”列表更新时,该列表在其他地方被修改,这很可能是因为UI相对较慢。同时从2个不同的位置访问列表可能会导致问题:即访问可能已被删除的元素。因此:在本地副本上工作将克服这个问题。这就是正在发生的事情。我添加了一个注释:“在本地副本上执行操作”。 - Stefan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接