从线程中加速实时GUI更新

5

这是我多年来使用的技术,用于接收网络数据并在我的GUI(对话框、表单等)中使用它。

    public delegate void mydelegate(Byte[] message);

    public ReceiveEngineCS(String LocalIpIn, String ReceiveFromIp, mydelegate d)
    {
         this.m_LocalIpIn = LocalIpIn;
         this.m_ReceiveFromIp = ReceiveFromIp;
         m_MainCallback = d;
         SetupReceive();
         m_Running = true;
         //Create the Track receive thread and pass the parent (this)
         m_RtdirReceiveThread = new Thread(new ParameterizedThreadStart(MessageRecieveThread));
         m_RtdirReceiveThread.Start(this);
    }

    private void MessageRecieveThread(System.Object obj)
    {
         ReceiveEngineCS parent = (ReceiveEngineCS)obj;

         while(parent.m_Running)
         {
              Byte[] receiveBytes = new Byte[1500];
              try
              {
                   receiveBytes = parent.m_ClientReceiver.Receive(ref parent.ipEndPoint);
                   parent.ThreadOutput(receiveBytes);   
              }
              catch ( Exception e )
              {
                  parent.StatusUpdate(e.ToString()); 
              }                         
         }          
    }

    public void ThreadOutput(Byte[] message)
    {
         m_MainCallback(message);           
    }

public partial class SystemMain : Form
{
    //Local Class Variables
    Network.ReceiveEngineCS SystemMessageReceiver;
    private void Form1_Load(object sender, EventArgs e)
    {
        //Load up the message receiver
        SystemMessageReceiver = new Network.ReceiveEngineCS(localAddy, fromAddy, new mydelegate(LocalDelegate));
    }

    public void LocalDelegate(Byte[] message)
    {
        if (Form.ListView.InvokeRequired)
        {
            //External call: invoke delegate
            Form.ListView.Invoke(new mydelegate(this.LocalDelegate), message);
        }
        else
        {
            //Get the Packet Header
            Formats.PacketHeaderObject ph = new Formats.PacketHeaderObject(message);
            //Update or Add item to Specific ListView
            ... update views
        }
    }
 }

接收器每秒接收10到100条实时消息,甚至更多。
最近我一直在研究.NET 4.0和C#,并注意到许多其他类似的数据处理方式,例如工作线程,以及使用委托和调用的其他方式。
我的问题是...在较新的.NET库(3.5、4.0等)中是否有更有效的方法来进行数据接收/ GUI更新?
我认为这种方法在C#中效果不佳。
非常感谢您的帮助。

1
每秒向用户发送一百个通知并没有太多意义,它看起来像一片模糊。首先使您的用户界面可用,这也解决了您的问题。Winforms中没有任何更改。 - Hans Passant
不错的想法,汉斯。如果我在线程中建立有关传入数据的元数据,并且每秒仅更新一次GUI,则仍会为数据提供体面的用户响应,同时允许GUI不会因更新而变得过于繁琐。通常,操作员使用已更新数据项的粗略计数作为数据速率的指南。因此,一眼就能看到每秒计数十个、每秒计数五十个等。但是,我可以将其作为元更新进行整合,并将其添加到列表视图中。 - Sleff
2个回答

4
将更新发布到GUI界面的最佳方法之一是使工作线程将更新中包含的数据打包并放入队列中。然后,UI线程会定期轮询该队列。在我看来,让工作线程使用Control.Invoke更新UI被过度使用了。相比之下,UI线程轮询更新具有以下几个优点:
  • 它打破了Control.Invoke强加的UI和工作线程之间的紧密耦合。
  • 它将更新UI线程的责任放在了UI线程上,这本来就应该如此。
  • UI线程可以决定何时以及多久更新一次。
  • 与由工作线程发起的调度技术相比,没有UI消息泵被淹没的风险。
  • 工作线程在继续执行其下一步之前不必等待确认已执行更新(即UI和工作线程都可以获得更高的吞吐量)。

您没有提到ThreadOutput是如何实现的,但如果尚未采用上述方法,则可以考虑该方法。经验告诉我,这通常是最好的方法。让UI线程控制其更新周期是一个很大的优势。


我一直面临的共享资源轮询数据的问题是时间控制。锁定和解锁共享队列会导致很多开销(当然我现在也有这个问题)。我研究了工作线程,发现线程有一种机制可以发送更新。我不确定的是,工作线程是否必须使用调用来更新UI所拥有的队列? - Sleff
队列的锁定与Control.Invoke相比,后者是一项极其昂贵的操作。此外,您可以始终使用使用无锁机制的ConcurrentQueue。工作线程不必使用Invoke将某些内容发布到队列中。首先称其为“UI拥有的队列”可能会有点误导。 - Brian Gideon
这对@Brian有所帮助。我不确定调用的开销。我一直看到它在更新来自简单文件或低速xml数据的GUI的简单示例中使用。我会查看ConcurrentQueue,并像@Hans建议的那样尝试减少需要更新的内容。 - Sleff
我想了解一下这个实现的具体情况。您是否在UI线程上创建一个共享队列(数组/字典),用于工作线程插入其消息?队列是否必须保留对每个要更新的UI控件的引用?您能给我们提供任何相关链接吗? - goku_da_master

1

不要每次都新建线程,可以看看 线程池


谢谢Zamboni,但实际上我只在应用程序的整个生命周期中使用一个线程来接收消息。这些消息是我需要随时接收的事件的连续更新。 - Sleff

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