У меня есть эта форма, которая порождает новый поток и начинает слушать и ждать 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 сек. Вы можете видеть, что это большая разница.
Есть ли способ улучшить это?
Проблема с вашим подходом заключается в том, что он обновляет пользовательский интерфейс для каждого отдельного пакета. Если вы получали 1000 пакетов каждую секунду, вы обновляли пользовательский интерфейс 1000 раз в секунду! Монитор, вероятно, не обновляется более 100 раз в секунду, и никто не сможет его прочитать, если он обновляется более 10 раз в секунду.
Лучший способ подойти к этой проблеме - поместить totalReceivedBytes += receivedBytes;
в поток, который обрабатывает ввод-вывод, и поместить таймер в поток пользовательского интерфейса, который выполняет Label.Text = totalReceivedBytes.ToString("##,0");
всего несколько раз в секунду. Когда передача начинается, запустите таймер; когда передача остановится, остановите таймер.
System.Windows.Forms.Timer
который вы помещаете в форму. Я не уверен в его проблемах, но это именно то, что вы хотите для этой задачи.
Да, есть способ улучшить это.
Во-первых, используйте 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, но эй, что они там! И, когда это делается, поток отключается, поскольку вы устанавливаете полный флаг. Вам также не нужно блокировать при настройке, так как дополнительный пробег в рабочем потоке не имеет значения в контексте.
BeginInvoke
вместо Invoke
, это может немного помочь. Но я верю, что настоящая проблема будет решена с помощью ответа @ Gabe выше. Обработчик событий никогда не будет вызываться из метода, который не требует вызова. По крайней мере, не в этой программе. Но спасибо за совет, это может оказаться полезным в будущем.
Вы пытались использовать BeginInvoke вместо Invoke? BeginInvoke() - это асинхронный вызов.
private void EVENTHANDLER_UpdateTransferProgress(long receivedBytes) {
if(InvokeRequired) {
BeginInvoke(new MethodInvoker(() => {
totalReceivedBytes += receivedBytes;
Label.Text = totalReceivedBytes.ToString("##,0");
}));
}
}