如何在C#中传输数据包时正确保持UI更新?

我有这种forms,它产生一个新的线程,并开始侦听并等待循环中的UDP数据包。 我需要的是保持UI更新接收的字节数。

为此,我设置了一个事件,我会在收到数据包后立即提出这个事件并将接收到的字节数作为参数传递。 由于我没有在UI线程上运行,我不能简单地直接更新UI。 这是我目前正在做的事情:

private void EVENTHANDLER_UpdateTransferProgress(long receivedBytes) { if(InvokeRequired) { Invoke(new MethodInvoker(() => { totalReceivedBytes += receivedBytes; Label.Text = totalReceivedBytes.ToString("##,0"); })); } } 

但这仍然在与数据包接收循环相同的线程上运行,并且它不会返回到该循环 – 并等待另一个数据包 – 直到此EVENTHANDLER_UpdateTransferProgress方法返回。

我的问题基本上是关于上述方法中的以下行:

 Label.Text = totalReceivedBytes.ToString("##,0"); 

像这样更新UI会减慢数据包接收速度。 如果我取消该行(或评论它),数据包接收将更快。

我怎么可能解决这个问题? 我认为更multithreading是关键,但我不确定如何在这种情况下正确实现它们……我正在使用带有.NET 2.0的Windows Forms。

编辑:

在我之前的测试中,上述情况似乎是正确的,实际上可能在某种程度上。 但经过一些测试后,我意识到问题出在整个Invoke(new MethodInvoker(() => { ... })); 事情。 当我删除它(当然不会更新UI)并离开EVENTHANDLER_UpdateTransferProgress但继续提升事件时,数据包接收速度要快得多。

我测试了接收一些文件,平均大约需要1.5秒,而不需要在事件处理程序上调用Invoke() 。 当我在事件处理程序中调用Invoke()时,即使没有更新UI中的任何控件或执行任何操作(换句话说,匿名方法体是空的),它花费的时间更长,约为5.5秒。 你可以看到它有很大的不同。

反正有没有改善这个?

您的方法的问题是它更新每个数据包的UI。 如果您每秒收到1000个数据包,则每秒更新UI 1000次! 显示器每秒刷新的次数可能不会超过100次,如果每秒更新超过10次,则没有人能够读取它。

解决这个问题的更好方法是将totalReceivedBytes += receivedBytes; 在处理I / O的线程中,并在执行Label.Text = totalReceivedBytes.ToString("##,0");的UI线程上放置一个计时器Label.Text = totalReceivedBytes.ToString("##,0"); 每秒最多只有几次。 传输开始时,启动计时器; 当传输停止时,停止计时器。

是的,有一种方法可以改善这一点。

第一种是使用BeginInvoke而不是Invoke ,它不会等待调用返回。 您还应该考虑在方法中使用其他表单

 private void EVENTHANDLER_UpdateTransferProgress(long receivedBytes) { if(InvokeRequired) { BeginInvoke(new Action(EVENTHANDLER_UpdateTransferProgress), receivedBytes)); return; } totalReceivedBytes += receivedBytes; Label.Text = totalReceivedBytes.ToString("##,0"); } 

因此,如果从不需要调用的方法调用此方法,则仍会执行GUI上的更新。


您可以做的另一个选择是破坏下载线程中的线程。 喜欢的东西

 public event EventHandler ReportProgress; public void startSendingUpdates(MonitorEventArgs args) { EventHandler handler = ReportProgress; if (handler == null) { return; } ThreadPool.QueueUserWorkItem(delegate { while (!args.Complete) { handler(this, args); Thread.Sleep(800); } }); } public void download() { MonitorEventArgs args = new MonitorEventArgs(); startSendingUpdates(args); while (downloading) { int read = downloadData(bytes); args.BytesTransferred += read; } args.Complete = true; } public class MonitorEventArgs : EventArgs { public bool Complete { get; set; } public long BytesTransferred { get; set; } } 

与好处相比,这种开销很小。 您的下载线程不受GUI更新的影响(至少与等待GUI更新相比)。 缺点是你在线程池中占据一个线程,但是,嘿,这就是他们的目的! 并且,线程在完成后关闭,因为您设置了完整标志。 设置时也不需要锁定,因为工作线程中的额外运行在上下文中不重要。

您是否尝试过使用BeginInvoke而不是Invoke? BeginInvoke()是一个异步调用。

 private void EVENTHANDLER_UpdateTransferProgress(long receivedBytes) { if(InvokeRequired) { BeginInvoke(new MethodInvoker(() => { totalReceivedBytes += receivedBytes; Label.Text = totalReceivedBytes.ToString("##,0"); })); } }