Microsoft Windows 7: рекомендации по улучшению стабильности приложений

Часть 2. Зависание приложений

Алексей Федоров

Зависание приложений с точки зрения операционной системы

Борьба с зависаниями приложений

 

В предыдущей статье данного цикла мы рассмотрели основные причины появления утечек памяти, а также привели рекомендации по корректному выделению памяти и ресурсов Windows, включая рекомендации для кода на С/С++, управляемого кода и клиентского кода веб-приложений.

Следующая тема, которая имеет непосредственное отношение к стабильности как приложений, так и самой системы, — это зависание приложений.

Зависание приложений с точки зрения операционной системы

По результатам многочисленных исследований, зависание приложений является второй после сбоев приложений причиной недовольства пользователей — они ожидают от приложений адекватной реакции на производимые ими действия и готовы ждать ответной реакции не более 5 с. Несмотря на то что приложение может быть занято выполнением каких-то вычислений или ожидать завершения операции ввода-вывода, в ряде случаев приложения просто перестают реагировать на команды пользователей.

Существует множество причин зависания приложений, и не все из них приводят к появлению пользовательского интерфейса, не реагирующего на команды, но основные усилия разработчиков системы направлены как раз на обеспечение отклика приложения и возможность исправления такой ситуации. Операционная система Windows автоматически обнаруживает такие приложения, собирает отладочную информацию и, при необходимости, принудительно завершает или перезапускает зависшие приложения. В ряде случаев пользователю может потребоваться либо завершение связанного с приложением процесса через Task Manager, либо даже перезапуск операционной системы.

Рассмотрим зависание приложения с точки зрения работы операционной системы. Когда один из потоков приложения создает окно на рабочем столе, то использует сервисы Desktop Window Manager (DWM) для обработки сообщений, приходящих этому окну. DWM помещает сообщения (события, связанные с манипуляцией мышью, вводом символов с клавиатуры, сообщения от других окон и т.п.) в очередь сообщений данного потока. Поток извлекает сообщения из очереди и обрабатывает их. Если поток не обслуживает очередь сообщений, вызывая функцию GetMessage(), окно «зависает» — оно не может ни перерисовать себя, ни принять команды от пользователя. Операционная система определяет состояние потока, прикрепляя таймер к сообщениям, находящимся в очереди. Если сообщение не было извлечено из очереди в течение 5 с, то DWM считает, что окно зависло. Для определения состояния окна следует использовать функцию IsHungAppWindow().

Нахождение зависшего окна — это лишь первый шаг к возможному решению проблемы. Пользователь все еще может завершить приложение нажатием кнопки Х, в результате чего окну будет послано сообщение WM_CLOSE, которое также попадет в очередь сообщений и застрянет там вместе с другими необработанными сообщениями. Desktop Window Manager пытается скрыть зависшее окно — вместо него отображается графическое изображение копии окна (оно называется window ghosting feature) c заголовком, содержащим надпись Not Responding. До тех пор пока поток, отвечающий за окно, не извлечет сообщения из очереди, DWM обрабатывает оба окна — оригинальное и его копию, но при этом позволяет пользователям взаимодействовать только с копией окна. Используя такое окно, пользователь может передвигать его, варьировать его размер, попытаться закрыть приложение, но не может изменить его внутреннее состояние.

Desktop Window Manager выполняет еще одну операцию за счет интеграции с механизмом Windows Error Reporting, при этом у пользователей появляется возможность не только завершить зависшее приложение, а иногда и перезапустить его, но и послать отладочную информацию на специальный сайт, поддерживаемый компанией Microsoft. Для получения возможности собирать данные для своих приложений разработчики должны подписаться на соответствующий сервис на сайте Winqual.

В Windows 7 добавлен еще один шаг: операционная система анализирует зависшее приложение и в ряде случаев позволяет пользователю завершить операцию, заблокировавшую приложение, и таким образом продолжить нормальную работу с ним. В текущей версии операционной системы поддерживается принудительное завершение заблокированных вызовов на уровне сокетов — в последующих версиях операционной системы будет поддерживаться большее число операций, которые могут быть принудительно завершены для разблокировки приложений.

Для того чтобы ваше приложение максимально корректно выходило из различных ситуаций, связанных с зависанием, нужно использовать следующие технологии, доступные как в Windows Vista, так и в Windows 7:

  • убедитесь в том, что приложение поддерживает механизм Application Restart and Recovery — корректно зарегистрированное приложение может быть автоматически перезагружено при практически полном сохранении данных. Этот механизм работает как при зависании приложений, так и при сбоях в работе приложений;
  • получайте информацию о частоте сбоев, а также о деталях каждого сбоя, произошедшего в вашем приложении, на сайте Winqual.

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

Борьба с зависаниями приложений

Как мы уже отметили, операционная система считает интерактивное приложение зависшим, если оно не обрабатывает сообщения из своей очереди более 5 с. Причинами зависания приложений могут быть как тривиальные ошибки, так и более глубокие, системные процессы. В любом случае пользователи должны получать приложения, которые максимально оперативно откликаются на команды. Далее приведены некоторые рекомендации по предотвращению зависания приложений:

  • убедитесь в том, что пользователи могут отменять операции, выполнение которых занимает более одной секунды, и/или производите операции в фоновом режиме и при необходимости отображайте ход длительных операций (рис. 1);

 

Рисунок

Рис. 1. Отображение хода операции

  • по возможности выполняйте длительные операции или операции, которые могут заблокировать интерфейс приложения, как фоновые задачи, помещаемые в очередь, — это может потребовать реализации механизма обмена сообщениями между потоком, отображающим интерфейс, и другими потоками;
  • сделайте код интерфейса вашего приложения максимально простым — по возможности не применяйте в нем вызовы функций, которые могут заблокировать интерфейс;
  • отображайте окна и диалоговые панели только тогда, когда они полностью заполнены информацией. Например, если диалоговая панель отображает информацию, для сбора которой требуется определенное время, сначала покажите упрощенный вариант диалоговой панели, а затем, по завершении сбора основной информации, обновите содержимое панели. Примером такого подхода может служить диалоговая панель свойств папки в Windows Explorer. В ней отображаются данные об общем размере файлов в папке — эти данные требуют некоторого времени для вычисления. Диалоговая панель отображает основные данные о папке, а поля Size и Size on disk заполняются после сбора данных из рабочего потока (рис. 2).

 

Рисунок

Рис. 2. Диалоговая панель Folder Properties

Рассмотрим некоторые рекомендации по снижению вероятности зависания приложений более подробно. Начнем с упрощения кода, отвечающего за пользовательский интерфейс.

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

При дизайне интерфейса приложения используйте следующие подходы:

  • перенесите код, выполнение которого занимает существенное время, в рабочие потоки;
  • идентифицируйте вызовы функций, которые могут заблокировать работу интерфейсного потока, и постарайтесь перенести их в другие рабочие потоки; при просмотре кода обращайте внимание на функции, вызывающие код в других динамически загружаемых библиотеках, — такие функции должны быть выведены из интерфейсного потока в первую очередь;
  • выведите из интерфейсного потока все операции ввода-вывода и вызовы сетевых функций — эти функции могут блокировать приложения на период от нескольких секунд до нескольких минут. Если в интерфейсном потоке требуется выполнение операций ввода-вывода, используйте асинхронные операции ввода-вывода;
  • обратите внимание на то, что интерфейсный поток также обслуживает все COM-серверы с моделью single-threaded appartment (STA), применяемые вашим приложением. Если приложение запустит «долгую» операцию, эти COM-серверы не будут доступны до возобновления обработки сообщений из очереди сообщений.

При проектировании интерфейса обращайте внимание на следующие потенциальные причины, которые могут привести к зависанию приложений:

  • не ожидайте завершения работы объектов уровня ядра (типа событий и мьютексов) дольше, чем минимально допустимый отрезок времени, — если вам все же необходимо дождаться завершения работы этих объектов, используйте функцию MsgWaitForMultipleObjects(), которая выведет поток из заблокированного состояния после получения нового сообщения;
  • не применяйте функцию AttachThreadInput() для распределения очереди сообщений между несколькими потоками — синхронизация доступа к очереди сообщений реализуется с помощью сложных алгоритмов, что в конечном счете может помешать Windows корректно определить зависшее окно;
  • не используйте функцию TerminateThread() для завершения рабочих потоков — при таком завершении могут остаться бесхозные синхронизационные объекты, блокировки и сигнальные события;
  • не вызывайте из интерфейсного потока код неизвестного происхождения — эта рекомендация в первую очередь относится к приложениям, поддерживающим модель расширения, — нет полной гарантии того, что разработчики расширений для вашего приложения придерживаются основных принципов написания приложений, эффективно реагирующих на команды пользователей;
  • не применяйте функцию SendMessage(HWND_BROADCAST) для передачи сообщений — любое приложение, которое некорректно обработает полученное сообщение, может заблокировать ваше приложение.

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

  • в потоке, который отвечает за интерфейс приложения, используйте асинхронные коммуникации — по возможности замените вызовы функции SendMessage() на подходящие функции — PostMessage(), SendNotifyMessage() или SendMessageCallback() — это позволит снизить вероятность блокировки вашего приложения;
  • применяйте фоновые потоки для выполнения операций, занимающих существенное время, или для задач, которые могут привести к блокировке приложения. Для создания рабочих потоков используйте новые программные интерфейсы — механизм пула потоков (thread pool). С помощью механизма пула потоков можно создавать коллекции рабочих потоков, способных эффективно обрабатывать асинхронные косвенные вызовы (asynchronous callbacks), получаемые приложениями. Пул потоков применяется для снижения числа прикладных потоков и более эффективного управления рабочими потоками. Приложения, использующие соответствующие программные интерфейсы (см. ниже), могут создавать очереди рабочих потоков, ассоциировать задачи со ссылками ожидания (waitable handles), заполнять очереди по таймеру, связывать очереди с процессами ввода-вывода и т.п. Приложения могут применять пул потоков для реализации следующих сценариев:
    • приложения могут параллельно распределять выполнение задач в виде множества небольших асинхронных элементов, например при выполнении распределенных запросов к индексам или сетевых операций,
    • использование пула потоков может упростить задачу управления потоками для приложений, создающих и управляющих большим числом потоков, каждый из которых существует относительно короткое время,
    • приложения могут применять пул потоков для параллельного выполнения фоновых задач;
  • обеспечьте возможность принудительного завершения операций, выполнение которых занимает длительное время. Для операций ввода-вывода, которые могут заблокировать приложение, применяйте функции ввода-вывода с возможностью отмены (I/O Cancellation). Эти фукции обеспечивают возможность завершения запросов на операции ввода-вывода, которые могут привести к повышенному использованию недоступных в данный момент ресурсов. Примерами таких функций являются CancelSynchronousIo() и CancelIoEx(). Отметим, что применение механизмов отмены операций ввода-вывода позволяет решить ряд проблем с такими операциями без принудительного завершения потоков и приложений;
  • для реализации асинхронных операций в коде на .NET используйте интерфейс IAsynchResult или класс Events.

Еще одним потенциальным источником блокировки вашего приложения может стать некорректный дизайн обработчиков ошибок и исключений. Обработка исключений позволяет разделить логику программы на две части: работу в штатном режиме и обработку ошибок. За счет этого разделения не всегда удается узнать точное состояние приложения непосредственно перед возникновением исключения, а обработчик исключения не всегда получает информацию, необходимую для восстановления состояния приложения для продолжения штатной работы после обработки ошибки. Это особенно важно в тех случаях, когда приложение применяет блокировки (locks), которые должны быть сняты в обработчике ошибки для предотвращения потенциальных дополнительных блокировок.

Вот несколько рекомендаций по корректной обработке ошибок и исключительных ситуаций:

  • не используйте блоки —try/except и функцию SetUnhandledExceptionFilter();
  • при применении исключений на уровне С++ используйте шаблоны типа auto_ptr для блокировок — блокировки должны быть сняты в деструкторе класса. Для обычных исключений освобождайте блокировки в блоке —finally;
  • обращайте особое внимание на код, который выполняется в обработчике исключения — в момент возникновения исключения ваше приложение уже может применять ряд блокировок, не добавляйте новые в коде обработчика исключения;
  • не обрабатывайте исключения без необходимости. Если вы используете обработчики стандартных исключений для сбора данных, рассмотрите возможность заменить их механизмом Windows Error Reporting;
  • не применяйте исключения на уровне С++ в потоке, отвечающем за интерфейс приложения.

Как мы уже отметили, на уровне операционной системы существует механизм Application Restart and Recovery, позволяющий перезапустить зависшие приложения или приложения, блокирующие какие-либо ресурсы без перезапуска самой операционной системы. О нем пойдет речь в следующих статьях данного цикла.

 

В начало В начало

КомпьютерПресс 12'2009

Наш канал на Youtube

1999 1 2 3 4 5 6 7 8 9 10 11 12
2000 1 2 3 4 5 6 7 8 9 10 11 12
2001 1 2 3 4 5 6 7 8 9 10 11 12
2002 1 2 3 4 5 6 7 8 9 10 11 12
2003 1 2 3 4 5 6 7 8 9 10 11 12
2004 1 2 3 4 5 6 7 8 9 10 11 12
2005 1 2 3 4 5 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
Популярные статьи
КомпьютерПресс использует