Как правильно обновлять пользовательский интерфейс при передаче пакетов в C #?

2

У меня есть эта форма, которая порождает новый поток и начинает слушать и ждать UDP-пакетов в цикле. Мне нужно, чтобы пользовательский интерфейс обновлялся с количеством полученных байтов.

Для этого я установил событие, которое я подниму, как только будет получен пакет, и передаст количество байтов, полученных в качестве аргумента. Поскольку я не запускаюсь в потоке пользовательского интерфейса, я не могу просто обновить интерфейс напрямую. Вот что я сейчас делаю:

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

Обновление пользовательского интерфейса, как это замедляет прием пакетов. Если я отключу эту строку (или прокомментирую ее), прием пакетов будет намного быстрее.

Как я могу решить эту проблему? Я думаю, что больше потоков является ключевым, но я не уверен, как правильно их реализовать в этой ситуации... Я использую Windows Forms с .NET 2.0.

EDIT:

В моем предыдущем тестировании вышеизложенное кажется истинным, и на самом деле это может быть в некоторой степени. Но после небольшого тестирования я понял, что проблема была в целом Invoke(new MethodInvoker(() => { ... }));. Когда я удалю это (пользовательский интерфейс не будет обновляться, конечно) и оставьте EVENTHANDLER_UpdateTransferProgress, но продолжайте поднимать событие, прием пакетов происходит намного быстрее.

Я тестировал получение некоторого файла, который принимал в среднем около ~ 1.5 сек без вызова Invoke() вообще в обработчике событий. Когда я вызывал Invoke() в обработчике событий, даже без обновления какого-либо элемента управления в пользовательском интерфейсе или при выполнении какой-либо операции (другими словами, тело анонимного метода было пустым), потребовалось гораздо больше времени около ~ 5.5 сек. Вы можете видеть, что это большая разница.

Есть ли способ улучшить это?

  • 0
    Какую версию .NET вы используете?
  • 0
    @SeanThoman Для этого конкретного приложения .NET 2.0.
Показать ещё 2 комментария
Теги:
multithreading
sockets
ui-thread
packet-capture

3 ответа

3
Лучший ответ

Проблема с вашим подходом заключается в том, что он обновляет пользовательский интерфейс для каждого отдельного пакета. Если вы получали 1000 пакетов каждую секунду, вы обновляли пользовательский интерфейс 1000 раз в секунду! Монитор, вероятно, не обновляется более 100 раз в секунду, и никто не сможет его прочитать, если он обновляется более 10 раз в секунду.

Лучший способ подойти к этой проблеме - поместить totalReceivedBytes += receivedBytes; в поток, который обрабатывает ввод-вывод, и поместить таймер в поток пользовательского интерфейса, который выполняет Label.Text = totalReceivedBytes.ToString("##,0"); всего несколько раз в секунду. Когда передача начинается, запустите таймер; когда передача остановится, остановите таймер.

  • 0
    Да, я в конечном итоге пришел к такому же выводу после публикации этого вопроса. Вероятно, это сильно ускорится ... Просто чтобы прояснить, вы говорите о таймере WinForms? Или есть лучшая альтернатива? Я продолжаю читать, что у таймера WinForms есть все виды проблем ... Но я не знаю альтернативы с каким-нибудь "тикером".
  • 0
    Да, это просто стандартный System.Windows.Forms.Timer который вы помещаете в форму. Я не уверен в его проблемах, но это именно то, что вы хотите для этой задачи.
Показать ещё 2 комментария
1

Да, есть способ улучшить это.

Во-первых, используйте 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");
}

Поэтому, если вы вызываете этот метод из метода, который не требует вызова, обновление по графическому интерфейсу все еще выполняется.


Другой вариант, который вы можете сделать, это перерыв потока в потоке загрузки. Что-то вроде

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

Накладные расходы на это небольшие по сравнению с преимуществами. Обновления графического интерфейса не влияют на ваш поток загрузки (по крайней мере, не по сравнению с ожиданием обновления графического интерфейса). Недостатком является то, что вы занимаете нить в threadpool, но эй, что они там! И, когда это делается, поток отключается, поскольку вы устанавливаете полный флаг. Вам также не нужно блокировать при настройке, так как дополнительный пробег в рабочем потоке не имеет значения в контексте.

  • 0
    Попробуем BeginInvoke вместо Invoke , это может немного помочь. Но я верю, что настоящая проблема будет решена с помощью ответа @ Gabe выше. Обработчик событий никогда не будет вызываться из метода, который не требует вызова. По крайней мере, не в этой программе. Но спасибо за совет, это может оказаться полезным в будущем.
  • 0
    @RicardoAmaral: концепция та же самая. В моей версии поток отправки отправляет обновления каждые 800 миллисекунд, в версии Гейба графический интерфейс имеет таймер, который обновляет определенное количество раз в секунду. Поэтому они делают то же самое ...
Показать ещё 2 комментария
0

Вы пытались использовать BeginInvoke вместо Invoke? BeginInvoke() - это асинхронный вызов.

private void EVENTHANDLER_UpdateTransferProgress(long receivedBytes) {
    if(InvokeRequired) {
        BeginInvoke(new MethodInvoker(() => {
            totalReceivedBytes += receivedBytes;
            Label.Text = totalReceivedBytes.ToString("##,0");
        }));
    }
}
  • 0
    Как уже предложил @Patrick (он был на пару минут быстрее, лол), я попробую это и посмотрю, поможет ли это, но, как я уже сказал, я прокомментировал его ответ, я считаю, что правильный ответ - это ответ Гейба. Нужно будет провести некоторое тестирование, хотя.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню