【问题标题】:How to properly keep the UI updated while transferring packets in C#?在 C# 中传输数据包时如何正确保持 UI 更新?
【发布时间】:2012-04-08 21:54:29
【问题描述】:

我有这个表单,它产生一个新线程并开始在循环中侦听和等待 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 会减慢数据包的接收速度。如果我把那条线去掉(或注释掉),数据包接收会快得多。

我怎样才能解决这个问题?我认为更多线程是关键,但我不确定如何在这种情况下正确实现它们......我正在使用带有 .NET 2.0 的 Windows 窗体。

编辑:

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

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

还有什么可以改进的吗?

【问题讨论】:

  • 您使用的是什么版本的 .NET?
  • @SeanThoman 对于这个特定的应用程序,.NET 2.0。
  • @SeanThoman 我已经实现了接受的答案,但我似乎没有注意到我原来的实现有任何区别。但是,我做了更多的测试,问题似乎出在 Invoke 本身上,这实际上是减慢了整个事情的速度。对于那个很抱歉。我必须更新问题...

标签: c# multithreading sockets packet-capture ui-thread


【解决方案1】:

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

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

【讨论】:

  • 是的,我在发布这个问题后最终得出了同样的结论。它可能会加快很多......只是为了澄清,你在谈论WinForms计时器?还是有更好的选择?我一直在读到 WinForms 计时器有各种各样的问题......但我不知道有某种“ticker”事件的替代方案。
  • 是的,这只是您在表单上输入的标准System.Windows.Forms.Timer。我不确定它的问题,但这正是你想要完成这项任务。
  • +1 这是正确的方法。 Invoke 阻塞调用线程。 BeginInvoke 不会,但在高信号率下,GUI 线程消息队列填满 gunge 的速度比清空速度快。 1 秒 Forms.Timer 是一种更好的方法。这是轮询是更好选择的少数几次之一:)
  • +1:这绝对是正确的方法。 InvokeBeginInvoke 被过度使用和高估了方式
【解决方案2】:

是的,有办法改善这一点。

第一个是使用BeginInvoke 而不是Invoke,它不会等待调用返回。您还应该考虑在您的方法中使用另一种形式

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

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


您可以做的另一个选择是中断下载线程中的线程。类似的东西

public event EventHandler<MonitorEventArgs> ReportProgress;

public void startSendingUpdates(MonitorEventArgs args) {
  EventHandler<MonitorEventArgs> 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,它可能会有所帮助。但我相信真正的问题将通过@Gabe 上面的回答得到解决。永远不会从不需要调用的方法调用事件处理程序。至少不在这个节目上。但是感谢您的提示,它在未来可能会很有用。
  • @RicardoAmaral:概念是一样的。在我的版本中,发送线程每 800 毫秒推送一次更新,在 Gabe 的版本中,GUI 有一个每秒更新一定次数的计时器。所以他们做同样的事情......
  • @RicardoAmaral:我不确定你是否注意到,但ThreadPool.QueueUserWorkItem 实际上启动了另一个线程,所以你的下载方法继续自行运行......所以基本上它是一个计时器,有一个重复 800 毫秒,当 args.Completetrue 时结束
  • 也许吧,但是 Gabe 的版本实现起来要简单得多,而且需要的代码也少得多 :)
【解决方案3】:

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

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

【讨论】:

  • 正如@Patrick 已经建议的那样(他快了几分钟哈哈),我会试试看是否有帮助,但就像我说我在他的回答中评论的那样,我相信正确的答案是 Gabe 的答案.不过需要进行一些测试。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-01
  • 2023-03-17
  • 2017-05-05
  • 1970-01-01
  • 2021-07-14
  • 1970-01-01
相关资源
最近更新 更多