Ни для кого не секрет, наличие инструментов делает жизнь проще. Тезис этот находит подтверждение как в повседневной жизни – чистить одежду удобнее и эффективнее щеткой, чем руками, заворачивать гайки гораздо проще ключом, чем пальцами – так и в профессиональной деятельности – кто сейчас возьмется строить, ладно, даже не дом, пусть всего лишь баню, при помощи одного лишь топора. Естественно программисты – не исключение. Значение инструментов, облегчающих путь от анализа постановки задачи до получения решения, готового к внедрению, трудно переоценить.
Процесс отладки в общем случае можно разбить на следующие шаги:
- определение факта наличия ошибки;
- поиск (локализация) ошибки;
- выяснение причин ошибки;
- определение способа устранения ошибки;
- устранение ошибки.
Кажется, что на первом шаге никакой инструмент не требуется. Запускаем программу и либо на некоторых исходных данных получаем неверные результаты, либо обнаруживаем, что некоторая последовательность действий по использованию программы ведет к ее «падению» или «зависанию». Однако для параллельной программы даже этот очевидный шаг может иметь существенную сложность. На практике нередко встречаются ситуации, когда неработоспособность параллельной программы проявляется один раз на сотню и более запусков. Очевидно, в этом случае инструментальная поддержка лишней не будет.
Второй шаг – поиск ошибки заключается в необходимости как можно более точной ее локализации, в идеале должна быть найдена переменная с неверным значением и/или строка кода, ведущая к краху программы. Типичный метод работы в этом пункте – использование режима трассировки в отладчике с наблюдением за состоянием переменных, регистров, стека вызова и т.д. «Плохая новость» – для многопоточных программ режим трассировки практически неприменим, поскольку автоматически меняет характер их выполнения, а значит, скрывает места, которые могут приводить к проблемам во время реальной работы. Кстати говоря, даже типичный способ локализации ошибки расстановкой операторов печати по тексту программы в этом случае нужно использовать с большой осторожностью – печать также вносит синхронизацию в выполнение программы.
Шаг третий – выяснение причин ошибки. Задача здесь – понять, почему ошибка возникла. Отсюда во многих случаях автоматически вытекает способ ее устранения (задача шага четвертого). Можно, конечно, выяснить условия, ведущие к проявлению ошибки (некое сочетание значений переменных, например) и просто вставить в код заплатку именно для этого случая. Данный вариант мы здесь не рассматриваем.
Типичные ошибки в многопоточных программах: гонки данных (data races), тупики (deadlocks), потоки в состоянии ожидания (stalled threads), потерянные сигналы (lost signals), заброшенные замки (abandoned locks).
Приведем краткое описание каждого вида.
- Гонки данных. Возникают, когда несколько потоков работают с разделяемыми данными и конечный результат зависит от соотношения скоростей потоков. Пусть, например, один поток выполняет над общей переменной x операцию x = x + 3, а второй поток – операциюx = x + 5. Данные операции для каждого потока фактически разбиваются на три отдельных подоперации: считать x из памяти, увеличить x, записать x в память. В зависимости от взаимного порядка выполнения потоками подопераций финальное значение переменной x может быть больше исходного на 3, 5 или 8. Гонка данных возможна и в случае, когда один поток пишет в переменную, а остальные только читают из нее.
- Тупики. Взаимная блокировка потоков, ожидающих наступление некоторого события для продолжения работы. Типичный пример тупика, когда нулевой поток занял для использования ресурс 1 и ожидает предоставления ему ресурса 2, а первый поток занял ресурс 2 и ожидает предоставления ему ресурса 1.
- Потоки в состоянии ожидания. Одно из состояний потока в многозадачной операционной системе – ожидание. Поток переходит в него, когда для продолжения выполнения ему требуется наступление некоторого внешнего события. Если пребывание потока в этом состоянии продолжается слишком долго, ITC рапортует об ошибке типа stalled thread. Интервал времени, по истечении которого выдается данная диагностика, может быть задан в настройках ITC.
- Потерянные сигналы. Возникают, когда поток ожидает наступление некоторого события, произошедшего прежде, чем поток пришел в состояние готовности к его приему и обработке. В результате поток никогда не сможет выйти из состояния ожидания.
- Заброшенные замки. Возникают в ситуации, когда поток захватил некоторый ресурс (критическую секцию, мьютекс) и был снят с выполнения по той или иной причине. В результате ресурс не может быть освобожден. Если он требуется другому потоку, это приведет к бесконечному ожиданию.
Список пакетов, используемых при отладке параллельных программ