Модернизация приложений

Часть 4. Обеспечение стабильности приложений

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

Утечки памяти

Использование ресурсов системы. Основные правила

Зависание приложений

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

 

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

Утечки памяти

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

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

  • выделение области «кучи» (heap) через функцию HeapAlloc() или ее эквиваленты для библиотеки языка С++ malloc или new. Для освобождения памяти следует использовать «парные» функции HeapFree(), free() и delete(). Отметим, что начиная с Windows Vista автоматически поддерживается так называемая низкофрагментированная «куча», применение которой позволяет снизить фрагментацию «кучи» — состояние, при котором в «куче» достаточно памяти для удовлетворения запроса на выделение памяти, но нет последовательной области необходимой длины;
  • прямое выделение памяти через функцию VirtualAlloc(). Для освобождения памяти, выделенной таким способом, следует использовать функцию VirtualFree();
  • применение ссылок (handle), полученных через функции CreateFile(), CreateEvent(), CreateThread(). Освобождение памяти осуществляется с помощью функции CloseHandle(), которой в качестве одного из параметров передается ссылка, полученная одной из перечисленных выше функций Create…();
  • использование ссылок, полученных через соответствующие функции подсистем USER и GDI. По умолчанию каждому процессу выделяется квота на 10 000 ссылок. Для каждой конкретной функции существует «парная» функция, освобождающая занятую память, — подробности об этом можно найти в документации к Windows SDK.

Для обнаружения утечек памяти необходимо следить за поведением приложения с течением времени. Для этого можно воспользоваться утилитой Windows Task Manager, которую мы кратко рассмотрели в предыдущей главе. Для обнаружения утечек памяти следует добавить к списку отображаемых колонок колонки Memory-Commit Size, Handles, User Objects и GDI Objects. Это позволит вам определить точку отсчета в потреблении ресурсов вашим приложением (рис. 1).

 

Рисунок

Рис. 1. Утилита Task Manager

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

  • средства мониторинга, входящие в состав Windows 7, — утилиты Performance Monitor и Resource Monitor (рис. 2 и 3);

 

Рисунок

Рис. 2. Утилита Performance Monitor

Рисунок

Рис. 3. Утилита Resource Monitor

  • средство тестирования приложений Application Verifier (рис. 4).

 

Рисунок

Рис. 4. Утилита Application Verifier

Помимо этих средств разработчикам доступны и другие:

  • для анализа выделения областей памяти в «куче» следует применять утилиту UMDH, входящую в состав Debugging Tools for Windows;
  • утилита XPerf также позволяет трассировать выделение памяти в «куче». Более подробно она рассматривается в главе «Измерение производительности системы и приложений».

Использование ресурсов системы. Основные правила

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

  • в коде на С++ используйте smart pointers — шаблонные классы, имитирующие обычные указатели с возможностью очистки, освобождения памяти, проверки границ и т.п., как для выделения памяти в «куче», так и для получения ресурсов Win32, включая ссылки. Библиотека C++ Standard Library содержит класс auto_ptr (описан в <memory>), который можно применять для выделения памяти в «куче». В состав библиотеки ATL входит множество классов для автоматического управления ресурсами на уровне как объектов кучи, так и ресурсов Win32;
  • используйте встроенные функции компилятора, например _com_ptr_t, для преобразования указателей на COM-интерфейсы в smart pointers и упрощения подсчета ссылок (reference). Для других COM-типов также существуют схожие классы — например _bstr_t и _variant_yt;
  • следите за применением памяти в коде на .NET. Обратите внимание на то, что код на .NET также подвержен утечкам памяти — это происходит из-за того, что сборщик мусора (garbage collector) не освобождает память до тех пор, пока существуют ссылки на нее;
  • в веб­приложениях утечки памяти могут возникать из-за циклических ссылок между COM-объектами и кодом на JScript. Internet Explorer 8, входящий в состав Windows 7, позволяет решить большинство проблем, связанных с такими утечками памяти. Для предыдущих версий браузера нужно использовать специальное отладочное средство — JavaScript Memory Leak Detector;
  • старайтесь не применять несколько вариантов завершения работы функции. Все выделения памяти, присвоенные переменным в области действия функции, должны быть освобождены перед ее завершением, желательно в одном блоке кода, доступном всегда независимо от поведения функции;
  • не используйте исключения без предварительного освобождения памяти, занятой всеми локальными переменными в области действия функции. При применении стандартных исключений предусмотрите освобождение памяти в блоке __finally. Если используются исключения С++, то все выделения памяти в «куче» и получение ссылок должны производиться через smart pointers;
  • не забывайте вызывать функцию PropVariantClear() перед удалением или повторной инициализацией объекта PROPVARIANT.

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

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

Зависание приложений

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

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

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

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

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

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

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

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

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

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

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

 

Рисунок

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

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

 

Рисунок

Рис. 6. Диалоговая панель 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, позволяющий перезапус­тить зависшие приложения или приложения, блокирующие какие­либо ресурсы без перезапуска самой операционной системы. Данный механизм используется, например, программами установки приложений — Windows Installer, а также ядром операционной системы для принудительного перезапуска приложений.

В следующей статье мы поговорим о механизме Application Restart and Recovery.

 

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

КомпьютерПресс 05'2011

Наш канал на 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
Популярные статьи
КомпьютерПресс использует