【问题标题】:Updating (lots of) GUI items using results of a background Thread使用后台线程的结果更新(大量)GUI 项目
【发布时间】:2011-07-04 08:56:15
【问题描述】:

我有一个 TreeView 控件,其中填充了项目(特定目录中的每个文件一个),以及一个在后台运行的线程,它用额外的数据缓慢地填充树中的每个节点。

然后我需要 TreeViewNode 根据结果更新其文本字段。

我尝试在处理每个项目后在工作人员上使用 BeginInvoke,但这会导致 GUI 线程变得无响应,因为后台工作人员正在处理的节点数量过多。 C# - Updating GUI using non-main Thread

public static void InvokeIfRequired(this System.Windows.Forms.Control c,
                                    Action action) {
    if (c.InvokeRequired) {
        c.Invoke((Action)(() => action()));
    }
    else {
        action();
    }
}

我尝试将多个节点更新批量处理到 GUI 的单个工作项中,并使用 BeginInvoke 一次更新多个,在此期间后台线程等待信号继续其工作,但这也会导致 GUI在 Invoked 方法期间变得无响应(似乎更新 TreeViewNode 是一项相当昂贵的操作?)

最后,我在 GUI 线程上创建了一个队列,由工作线程填充,并在应用程序空闲时进行轮询,效果很好,但感觉就像一个完整的 hack。

using System;
using System.Runtime.InteropServices;

namespace System
{
    struct PointAPI
    {
        public Int32 x;
        public Int32 y;
    }

    struct WindowsMessage
    {
        public Int32 hwnd;
        public Int32 message;
        public Int32 wParam;
        public Int32 lParam;
        public Int32 time;
        public PointAPI pt;
    }

    static class IdlePolling
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool PeekMessage(ref WindowsMessage lpMsg,
                                                Int32 hwnd,
                                                Int32 wMsgFilterMin,
                                                Int32 wMsgFilterMax,
                                                Int32 wRemoveMsg);
        static public bool HasEventsPending()
        {
            WindowsMessage msg = new WindowsMessage();
            return PeekMessage(ref msg, 0, 0, 0, 0);
        }
    }
}

...
Application.Idle += mainForm.OnIdle;
...

    public partial class MainWindow : Form
    {
        ...
        public void OnIdle(object sender, EventArgs e)
        {
            while (IdlePolling.HasEventsPending() == false)
            {
                ConsumeGUIUpdateItem();
                Thread.Sleep(50);
            }
        }
    }

那么更新大量gui项目的“正确”方法是什么,不会导致GUI线程在进程中挂起。

【问题讨论】:

  • 对 UI 的经典滥用。 UI 是面向用户的,一个用户不能处理超过几十个项目,所以你不应该加载数千个。
  • 过去我只是简单地编写了一些代码来将每次更新的事件推送到 UI 线程中。它非常适合每秒 100 次更新。
  • @Henk - 可能会突然添加项目,让用户有足够的时间与 treeView 进行交互?奇怪的是,UI 显然可以处理更新,因为 'complete hack' 有效,那么为什么 BeginInvoke 的性能如此糟糕?
  • “hack”有效,因为它只在主窗体的消息队列为空并且其中有一个 Sleep 时执行。这意味着一旦表单想要重新绘制,OnIdle 函数会在下一个项目完成更新后返回,并且在再次收到 Application.OnIdle 事件之前不会更新任何项目。
  • @MartinJames 我猜 BeginInvoke 性能很差,因为每一帧,后台线程可能已经要求主线程做 N 项工作,而 GUI 线程跟不上背负重担,忽略其他信息?

标签: c# multithreading user-interface


【解决方案1】:

russ,不错的尝试。总之:我认为解决方法是 invoke 批处理而不是 begininvoke()。让我解释一下:

当我在 WPF 中的数据网格中添加许多项目时,我遇到了类似的问题,工作人员将 开始调用(或 WPF 中的等效项)到 UI 线程。我也发现在高调用下,新项目的插入效率不高。对于这个有趣的特定设置,asnc 调用占了很大比例的开销。

最后,我采用了一种更简单的方法,仅插入批处理(就像您所做的那样),而是通过异步线程编组插入它们,而是将 WPF 等同于 同步 Invoke() 而不是 async BeginInvoke()。

您可能知道,每次您begininvoke() 时,.net 都会在 Windows 消息泵 中放置一条消息来处理调用。但是,如果您的应用正忙于处理其他消息,则插入可能需要一段时间。同样,太多的 begininvokes() 会阻碍其他 Windows 消息泵消息的处理。最终,当发送过多时,窗口可能会丢弃多余的,因此您的方法可能永远不会被调用。正如您可能知道的那样,无论是 c++ 还是 .net,这都是正确的。

最终,Windows 在 UI 线程中一次只能做一件事,无论是执行典型的 GUI 内容还是处理您的异步方法。

干杯

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-30
    • 2019-05-11
    • 2015-05-10
    • 1970-01-01
    • 2017-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多