Найти в Дзене

.NET (Framework) WinForms: Подводные камни Invoke

Если Вы когда-либо разрабатывали многопоточное приложение, в котором рабочие потоки часто взаимодействуют с потоком пользовательского интерфейса, то скорее всего Вы уже натыкались на эту проблему - внезапные исключения в Invoke при завершении работы.

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

Итак, в чем же дело? Ответ лежит внутри особенностей реализации Invoke - и в принципах функционирования ОС Windows (начиная с, как минимум, Windows 95, и, возможно, даже с более ранних версий).

Начну немного издалека. Надеюсь, все знают, что Windows - это, по сути, событийно-ориентированная система, в которой приложения с UI обязаны поддерживать обработку хотя бы одной очереди сообщений и соответствующего цикла обработки сообщений?

На этом месте как минимум половина известных мне программистов или изображает на лице вопрос "Щто?", или начинает неуверенно кивать головой - да, было дело, что-то такое вспоминается, но как-то смутно. Ну, тема эта слишком обширна и слишком фундаментальна, так что рекомендую: если Вы не в курсе, что все это такое, то ищите толковые книги по, скажем, программированию под Windows API (Win32 API, как вариант). Именно книги - поскольку хорошей технической документации, рассчитанной на начинающих, я что-то не встречал. С книгами там тоже не все здорово, но шанс есть. Я же изучал эту тему слишком давно, так что посоветовать что-либо не берусь.

Вернемся же к Invoke. Итак, суть проблемы, похоже в том, что передача вызова в поток UI происходит с участием очереди сообщений и какого-нибудь сообщения вида WM_COMMAND, причем с сообщением передается адрес коллбэка. То есть сообщение становится в очередь, и, когда оно попадает на исполнение, в потоке UI вызывается тот метод, который был указан в Invoke.

А теперь вспомним возможные проблемы с обработкой очереди сообщений UI. Собственно, нас интересуют всего два:

  1. Если приложение не успевает обрабатывать сообщения, они могут теряться (по крайней мере, так было раньше, сейчас - не проверял)
  2. Сообщение может прийти в тот момент, когда целевой контрол фактически прекратил свое существование

В применении к Invoke и .NET это означает:

  1. Вызов Invoke может никогда не завершится (крайне маловероятная ситуация, причем означающая, что с архитектурой приложения что-то серьезно не так). Толком не исследовал, но несколько раз сталкивался лет 15 назад.
  2. Несмотря на то, что в момент начала вызова Invoke все было в порядке, на момент отработки делегата, переданного в Invoke, отдельные контролы или даже все окно, к которому идет обращение, могут быть в состоянии disposed. Закрыты и уничтожены, так сказать. И вот это вполне рядовая ситуация

Теперь о том, как эту проблему решить. Основная идея: в приложении не должно быть кода, которому требовалось бы гарантированное нормальное завершение Invoke! Если Вам действительно требуется что-то такое, используйте другие механизмы взаимодействия потоков. Invoke же - только для передачи данных для UI, которые вполне можно потерять, если контролы / окна были удалены / закрыты.

Для того, чтобы не было проблем с исключениями, на stackoverflow было предложено примерно такое решение:

SafeInvoke вместо Invoke
SafeInvoke вместо Invoke
private delegate object SafeInvokeCallback(Control control, Delegate method, params object[] parameters);

public static object SafeInvoke(this Control control, Delegate method, params object[] parameters)
{
if (control == null)
throw new ArgumentNullException("control");

if (control.InvokeRequired)
{
IAsyncResult result =
null;
try
{
result = control.BeginInvoke(
new SafeInvokeCallback(SafeInvoke), control,
method, parameters);
}
catch (InvalidOperationException)
{ /* This control has not been created
or was already (more likely) closed. */ }

if (result != null)
try {
return control.EndInvoke(result);
}
catch (InvalidOperationException)
{
return null;
}
}
else {
if (!control.IsDisposed)
return method.DynamicInvoke(parameters);
}

return null;
}

Вместо обычного Invoke будем использовать SafeInvoke, и все будет хорошо. Наверное.