Я по-прежнему сталкивается с фоновым потоком в пользовательском интерфейсе WinForm. Зачем? Вот некоторые из проблем:
- Очевидно, самая важная проблема, я не могу изменить элемент управления, если я не выполняю тот же поток, который его создал.
- Как вы знаете, Invoke, BeginInvoke и т.д. недоступны до тех пор, пока не будет создан элемент управления.
- Даже после того, как RequiresInvoke возвращает true, BeginInvoke все равно может отбрасывать ObjectDisposed и даже если он не бросает, он никогда не сможет выполнить код, если элемент управления будет уничтожен.
- Даже после того, как RequiresInvoke возвращает true, Invoke может бесконечно зависать в ожидании выполнения с помощью элемента управления, который был удален одновременно с вызовом Invoke.
Я ищу элегантное решение этой проблемы, но прежде чем я узнаю, что я ищу, я подумал, что я проясню проблему. Это должно принять общую проблему и поставить для нее более конкретный пример. В этом примере допустим, что мы передаем большие объемы данных через Интернет. Пользовательский интерфейс должен иметь возможность отображать диалог прогресса для уже запущенной передачи. Диалог прогресса должен обновляться постоянно и быстро (обновляется от 5 до 20 раз в секунду). Пользователь может в любое время отклонить диалог прогресса и снова вызвать его, если это необходимо. И далее, давайте притворяемся аргументами, что если диалог виден, он должен обработать каждое событие прогресса. Пользователь может нажать "Отмена" в диалоговом окне выполнения и с помощью изменения аргументов событий, отмените операцию.
Теперь мне нужно решение, которое будет соответствовать следующему полю ограничений:
- Разрешить рабочему потоку вызывать метод в элементе управления/форме и блокировать/дождаться завершения выполнения.
- Разрешить самому диалогу вызывать этот же метод при инициализации или тому подобное (и, следовательно, не использовать invoke).
- Не устанавливайте нагрузку на реализацию метода обработки или вызывающего события, решение должно только изменить сам подписку на события.
- Соответственно обрабатывать блокирующие вызовы в диалог, который может находиться в процессе утилизации. К сожалению, это не так просто, как проверка IsDisposed.
- Должно быть возможно использовать любой тип события (предположим делегат типа EventHandler)
- Не следует переводить исключения в TargetInvocationException.
- Решение должно работать с .Net 2.0 и выше
Итак, можно ли это решить с учетом ограничений выше? Я искал и вырыл бесчисленные блоги и дискуссии, и, увы, я все еще с пустыми руками.
Обновление: я понимаю, что этот вопрос не имеет простого ответа. Я был на этом сайте всего пару дней, и я видел некоторых людей с большим опытом, отвечающим на вопросы. Я надеюсь, что один из этих людей решил это достаточно достаточно для того, чтобы я не потратил неделю или около того, чтобы принять разумное решение.
Обновление № 2: Хорошо, я попытаюсь описать проблему немного подробнее и посмотреть, что (если что-либо) вытряхивается. Следующие свойства, которые позволяют нам определить его состояние, имеют пару вещей, вызывающих беспокойство...
-
Control.InvokeRequired = Документировано для возврата false, если выполняется в текущем потоке, или если IsHandleCreated возвращает false для всех родителей. Я смущен реализацией InvokeRequired, имеющей возможность либо бросить ObjectDisposedException, либо потенциально даже воссоздать дескриптор объекта. И поскольку InvokeRequired может возвращать true, когда мы не можем вызвать (Dispose in progress), и он может возвращать false, даже если нам может понадобиться использовать invoke (Create in progress), это просто не может быть доверено во всех случаях. Единственный случай, когда я могу увидеть, где мы можем доверять InvokeRequired return false, - это когда IsHandleCreated возвращает true как до, так и после вызова (BTW, документы MSDN для InvokeRequired указывают на проверку для IsHandleCreated).
-
Control.IsHandleCreated = Возвращает true, если дескриптор был назначен элементу управления; в противном случае - false. Хотя IsHandleCreated - безопасный вызов, он может разбиться, если элемент управления находится в процессе воссоздания его дескриптора. Эта потенциальная проблема, по-видимому, может быть решена путем выполнения блокировки (контроля) при доступе к IsHandleCreated и InvokeRequired.
-
Control.Disposing = Возвращает true, если элемент управления находится в процессе утилизации.
- Control.IsDisposed = Возвращает true, если элемент управления удален. Я рассматриваю возможность подписки на событие Disposed и проверку свойства IsDisposed, чтобы определить, будет ли BeginInvoke когда-либо завершаться. Большая проблема здесь заключается в отсутствии блокировки синхронизации, которая препятствует переносу Disposing → Disposed. Возможно, если вы подписаны на событие Disposed и после этого убедитесь, что Disposing == false && IsDisposed == false вы все еще можете никогда не видеть огонь Disposed event. Это связано с тем, что реализация Dispose sets Disposing = false, а затем устанавливает Disposed = true. Это дает вам возможность (хотя и небольшую), чтобы читать как Disposing, так и IsDisposed как ложные на удаленном элементе управления.
... у меня болит голова:( Надеюсь, что вышеприведенная информация проливает немного больше света на проблемы для тех, у кого есть эти проблемы. Я ценю ваши запасные мыслительные циклы на этом.
Закрытие проблемы... Ниже приведена половина метода Control.DestroyHandle():
if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
lock (this.threadCallbackList)
{
Exception exception = new ObjectDisposedException(base.GetType().Name);
while (this.threadCallbackList.Count > 0)
{
ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
entry.exception = exception;
entry.Complete();
}
}
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
this.window.DestroyHandle();
}
Вы заметите, что ObjectDisposedException отправляется во все вызовы с перекрестными потоками. Вскоре после этого вызывается вызов this.window.DestroyHandle(), который, в свою очередь, уничтожает окно и устанавливает его дескриптор ссылки на IntPtr.Zero, тем самым предотвращая дальнейшие вызовы в методе BeginInvoke (точнее MarshaledInvoke, которые обрабатывают как BeginInvoke, так и Invoke). Проблема здесь в том, что после того, как блокировка освободится от threadCallbackList, новая запись может быть вставлена до того, как поток управления закроет дескриптор окна. Похоже, что я вижу, хотя и нечасто, достаточно часто, чтобы остановить выпуск.
Обновление # 4:
Извините, что продолжайте перетаскивать это; однако, я думал, что стоит документировать здесь. Мне удалось решить большинство проблем выше, и я сужусь над решением, которое работает. Я ударил еще одну проблему, о которой я беспокоился, но до сих пор не видел "in-the-wild".
Эта проблема связана с гением, который написал свойство Control.Handle:
public IntPtr get_Handle()
{
if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
{
throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
}
if (!this.IsHandleCreated)
{
this.CreateHandle();
}
return this.HandleInternal;
}
Это само по себе не так плохо (независимо от моего мнения о получении {} модификаций); однако в сочетании с свойством InvokeRequired или методом Invoke/BeginInvoke это плохо. Вот основной поток Invoke:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
Проблема здесь в том, что из другого потока я могу успешно передать первый оператор if, после которого дескриптор уничтожается управляющим потоком, что приводит к тому, что get из свойства Handle заново создает дескриптор окна в моем потоке, Это может привести к возникновению исключения в исходном потоке управления. На самом деле, я действительно смущен, потому что нет никакого способа защититься от этого. Если бы они использовали свойство InternalHandle и тестировались на результат IntPtr.Zero, это не было проблемой.