Что вы ищете при отладке тупиков?


25

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

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

Что вы ищете при отладке тупиков?


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

Стратегии, о которых я могу думать, - это запись в журнал (как отметили несколько других), на самом деле проверка графика тупиковой ситуации, когда кто-то ждет, когда кто-то удерживает блокировку (см. Stackoverflow.com/questions/3483094/… для некоторых указатели) и блокировать аннотации (см. clang.llvm.org/docs/ThreadSafetyAnalysis.html ). Даже если это не ваш код, вы можете попытаться убедить автора добавить аннотации - они, вероятно, найдут ошибки и исправят их (возможно, в том числе и ваши) в процессе.
Дон Хэтч

Ответы:


23

Если ситуация является реальной тупиковой ситуацией (то есть два потока удерживают две разные блокировки, но по крайней мере один поток хочет блокировки, которую удерживает другой поток), вам необходимо сначала отказаться от всех предварительных представлений о том, как потоки упорядочивают блокировку. Ничего не предполагать Возможно, вы захотите удалить все комментарии из кода, который вы просматриваете, поскольку эти комментарии могут заставить вас поверить в то, что не соответствует действительности. Трудно подчеркнуть это достаточно: ничего не предполагайте.

После этого определите, какие блокировки удерживаются, пока поток пытается заблокировать что-то еще. Если вы можете, убедитесь, что поток разблокируется в обратном порядке от блокировки. Более того, убедитесь, что поток удерживает только одну блокировку за раз.

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

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


4
+1 Вау, это пессимистично ... правда, правда. Это дано, что вы не можете найти все ошибки. Спасибо за предложения!
Майкл К

Брюс, твоя характерная черта "настоящего тупика" удивляет меня. Я думал, что тупик между двумя потоками - это когда каждый ожидает блокировки, которую держит другой. Кажется, ваше определение также включает случай, когда поток, удерживая одну блокировку, ожидает получения второй блокировки, которая в данный момент удерживается другим потоком. Это не похоже на тупик для меня; это??
Дон Хэтч

@ DonHatch - я плохо сформулировал это. Ситуация, которую вы описываете, не тупиковая. Я надеялся передать беспорядок отладки ситуации, которая включает в себя поток, удерживающий блокировку A, затем пытающийся получить блокировку B, в то время как поток, который удерживает блокировку B, пытается получить блокировку A. Возможно. Или, может быть, ситуация намного сложнее. Вам просто нужно очень непредвзято относиться к порядку приобретения замка. Изучите все предположения. Ничего не верь.
Брюс Эдигер

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

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

  2. Определите замки, которые участвуют. Поменяйте все мьютекс / семафоры, которые ждут вечно на время ожидания ... что-то смехотворно длинное, как 5 минут. Зарегистрируйте ошибку, когда она истечет. Это, по крайней мере, укажет вам направление на один из замков, который вовлечен в проблему. В зависимости от изменчивости времени вам может повезти, и вы найдете оба замка после нескольких пробежек. Используйте код / ​​условия ошибки функции для записи трассировки псевдостека после того, как таймерное ожидание не сможет определить, как вы попали туда в первую очередь. Это должно помочь вам определить поток, который вовлечен в проблему.

  3. Еще одна вещь, которую вы можете попробовать - это создать библиотеку-оболочку вокруг ваших сервисов mutex / семафоров. Отследите, какие потоки имеют каждый мьютекс и какие потоки ожидают мьютекс. Создайте поток монитора, который проверяет, как долго блокировались потоки. Запустите в течение некоторого разумного периода времени и сохраните информацию о состоянии, которую вы отслеживаете.

В какой-то момент потребуется проверка старого старого кода.


6

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

Другой вариант - удаление частей кода и данных для локализации ошибки. Может быть, даже написать небольшую программу, которая берет только некоторые из классов и выполняет только самые основные тесты (конечно, в нескольких потоках). Удалите все, что связано с графическим интерфейсом, например, любой вывод о фактическом состоянии обработки. (Я обнаружил, что пользовательский интерфейс является источником ошибки достаточно часто)

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


«Исключения могут помешать освобождению» -> Мне жаль языки, у которых нет переменных области действия: /
Матье М.

1
@Matthieu: Наличие переменных областей и их правильное использование могут быть двумя разными вещами. И он спросил о возможных проблемах вообще, не упоминая конкретный язык. Так что это одна вещь, которая может повлиять на поток контроля.
Торстен Мюллер

3

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

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


1
Сегодня я отладил пару таких мертвых замков. Хитрость заключалась в том, чтобы обернуть pthread_mutex_lock () макросом, который печатает функцию, номер строки, имя файла и имя переменной мьютекса (путем его токенизации) до и после получения блокировки. Сделайте то же самое для pthread_mutex_unlock () тоже. Когда я увидел, что моя ветка заморозилась, мне просто нужно было просмотреть последние два сообщения, две цепочки пытались заблокировать, но никогда не заканчивали! Теперь все, что осталось, это добавить механизм для переключения во время выполнения. :-)
Plumenator

3

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

Если это не удастся, как сказал Петер Тёрёк, то, вероятно, подойдет регистрация. Насколько я знаю, Debugger плохо работал в многопоточной среде. попытайтесь определить, где находится замок, узнать, какие ресурсы ждут, и в каком состоянии происходит гонка.


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

@gbjbaanb, я думаю, сказать, что это твой враг, слишком суров. Возможно, было бы правильно сказать, что это ваш лучший друг, который иногда вас подводит. Я согласен с несколькими другими людьми на этой странице, которые говорят, что регистрация - это хороший первый шаг, после того, как проверка кода не удалась - часто (фактически, по моему опыту, в большинстве случаев), простая стратегия ведения журналов находит проблема легко, и все готово. В противном случае во что бы то ни стало прибегнуть к другим методам, но я не думаю, что это хороший совет, чтобы избегать того, что чаще всего является лучшим инструментом для работы только потому, что он не всегда полезен.
Дон Хэтч

0

Этот вопрос меня привлекает;) Прежде всего, считайте, что вам повезло, поскольку вы смогли воспроизводить проблему последовательно при каждом запуске. Если вы получаете одно и то же исключение с одной и той же трассировкой стека каждый раз, то это должно быть довольно просто. Если нет, то не стоит так сильно доверять трассировке стека, вместо этого просто следите за доступом к глобальным объектам и их изменениям состояния во время выполнения.


0

Если вам нужно отладить тупики, у вас уже есть проблемы. Как правило, используйте замки в кратчайшие сроки - или не используйте их вообще, если это возможно. Следует избегать любой ситуации, когда вы берете блокировку и затем переходите к нетривиальному коду.

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

И затем есть старая, но безошибочная стратегия: назначьте «уровень» каждой блокировке, начиная с уровня 0. Если вы берете блокировку уровня 0, вам не разрешают другие блокировки. После взятия блокировки уровня 1 вы можете взять блокировку уровня 0. После взятия замка уровня 10 вы можете взять замки на уровне 9 или ниже и т. Д.

Если вы обнаружите, что это невозможно сделать, вам нужно исправить свой код, потому что вы попадете в тупики.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.