Отладка приложений

         

Требования утилиты DeadlockDetection


Обратите внимание, что в предыдущем разделе не высказывалось никаких предположений относительно того, что нужно делать, когда неожиданная блокировка парализует вашу программу. Предложены были скорее профилактические меры, с помощью которых можно попробовать, в первую очередь, избежать блокировок, а не предписания для их исправления при возникновении. В этом разделе показано, что исправление блокировок задача нелегкая, даже с учетом применения отладчика, и что почти всегда нужна некоторая дополнительная помощь. Такую помощь и предоставляет утилита DeadlockDetection.

Вот список основных требований, учтенных при разработке DeadlockDetection:

1. Утилита должна точно показывать, где в коде пользователя происходит блокировка. Инструмент, который только сообщает, что функция EnterCriticaiSection блокирована, не очень помогает. Действительно эффективный инструмент должен возвращаться обратно к адресу, и, следовательно, к исходному файлу и номеру строки, где произошла блокировка, чтобы имелась возможность ее быстро исправить.

2. Утилита должна показывать, какой объект синхронизации стал причиной блокировки.

3. Утилита должна показывать, какая Windows-функция блокирована и какие параметры ей переданы. Это помогает увидеть как значения тайм-аута, так и значения параметров, переданных в функцию.

4. Утилита должна определить, какой поток вызвал блокировку.

5. Утилита должна быть достаточно "легковесной", т. е. как можно меньше вмешиваться в работу программы пользователя.

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

7. Утилита должна легко объединяться с программами пользователя.

Имейте в виду, что утилиты типа DeadlockDetection определенно воздействуют на поведение приложения, которое они наблюдают. Утилита DeadlockDetection сама может привести к блокировкам ваших программ, потому что работа, которую она делает, чтобы собирать информацию, замедляет выполнение потоков.
Я определил это поведение чуть ли не как свойство, потому что как только вы вызываете блокировку кода, считайте, что вы уже идентифицировали ошибку, а это — первый шаг к ее исправлению. Кроме того, всегда лучше самому находить ошибки, чем ждать, пока это сделают заказчики.



История отладочной войны Блокировка

Сражение

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

Продукт, над которым они работали, имел интересную архитектуру и был в значительной степени многопоточным. Блокировка, с которой они столкнулись, происходила только в определенное время, и это всегда случалось в середине последовательности загрузок из библиотеки динамической компоновки (DLL). Программа попадала в блокировку, когда вызывалась функция WaitForSingleObject, проверяющая способность потока создавать некоторые разделяемые объекты.

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

Результат

Всегда с удовольствием вспоминаю эту ситуацию, потому что это был один из тех немногих случаев, когда уже через 5 минут после запуска отладчика я стал похож на героя. Как только команда дублировала блокировку, я быстро взглянул на окно Call Stack и заметил, что программа ожидала на дескрипторе потока внутри функции DllMain. Когда загружается некоторая DLL, эта функция, являясь частью архитектуры DLL, стартует другой поток и затем немедленно вызывает функцию WaitForSingleObject из объекта события подтверждения приема, чтобы гарантировать, что порожденный поток способен должным образом инициализировать некоторые важные разделяемые объекты перед продолжением остальной части обработки в DllMain.

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


Операционная система использует эту секцию, чтобы синхронизировать различные действия, которые случаются "за сценой" процесса. Одной из ситуаций, в которых используется критическая секция, является сериализация1выполнения DllMain для следующих четырех случаев ее вызова: DLL_PROCESS_ATTACH (присоединение DLL-процесса), DLL_THREAD_ATTACH (присоединение DLL-потока), DLL_THREAD_ DETACH (отсоединение DLL-потока) и DLL_PROCESS_DETACH (отсоединение DLL-процесса). Причину обращения к DliMain указывает ее второй параметр.

 Сериализация — преобразование в последовательную форму. — Пер.

В приложении, над которым работала команда, запрос к LoadLibrary заставил операционную систему захватить критическую секцию процесса для того, чтобы вызывать DliMain для случая DLL_PROCESS_ATTACH. Затем функция DliMain порождала второй поток. Всякий раз, когда процесс порождает новый поток, операционная система захватывает критическую секцию процесса так, чтобы она могла вызывать функцию DliMain каждой загружаемой DLL для случая DLLJTHREAEKATTACH. В этой конкретной программе второй поток блокировался, потому что критическую секцию процесса содержал первый поток. К сожалению, первый поток затем вызывал функцию WaitForSingleObject, чтобы гарантировать, что второй поток способен должным образом инициализировать некоторые разделяемые объекты. Поскольку второй поток был блокирован на критической секции процесса, удерживаемой первым потоком, а первый поток блокирован при ожидании второго потока, результатом была обычная взаимоблокировка.

Урок

Очевидный урок таков: необходимо избегать любых вызовов ожидающих (Wait*-) функций внутри DliMain. Однако проблемы с критической секцией процесса касаются не только Юа11:*-функций. Операционная система использует критическую секцию процесса и при вызове других фунций (CreateProcess, GetModuleFileName, GetProcAddress, LoadLibrary И FreeLibrary), так ЧТО не нужно вызывать любую из этих функций в DliMain. Поскольку DliMain обзаводится критической секцией процесса, то она может одновременно выполнять только один поток.

Итак, даже опытные разработчики могут допускать многопоточные ошибки—и, как я уже говорил ранее, этот вид ошибок часто происходит в том месте кода, где их меньше всего ожидают.



Содержание раздела