При взаимной блокировке двух горутин тест зависает, но race не сообщает об ошибке. Почему race detector не ...

При взаимной блокировке двух горутин тест зависает, но -race не сообщает об ошибке. Почему race detector не обязан обнаруживать такую проблему?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

Race detector появился для поиска трудно воспроизводимых состояний гонки: параллельных чтений и записей одной области памяти, между которыми нет отношения happens-before. Такие ошибки могут приводить к повреждению данных, хотя программа внешне продолжает выполняться.

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

Постановка проблемы

Например, одна горутина удерживает mutex A и ждёт mutex B, а другая удерживает mutex B и ждёт mutex A. Общие данные при этом не обязаны читаться или изменяться без синхронизации, поэтому race detector не находит нарушения.

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

Подробное решение

Race detector инструментирует обращения к памяти и операции синхронизации, после чего проверяет, существуют ли конфликтующие обращения без установленного отношения happens-before. Он не строит универсальный граф ожидания всех блокировок и не доказывает отсутствие циклов ожидания.

Взаимная блокировка может быть race-free: каждая общая переменная защищена, но порядок захвата нескольких mutex различается. Поэтому прохождение тестов с -race не означает отсутствия deadlock.

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

Нужно различать и другие дефекты: starvation, livelock и слишком долгую блокировку. Они также не обязаны быть обнаружены race detector. Инструмент может менять временное поведение программы, поэтому успешный или неуспешный запуск с -race не заменяет проверку корректности протокола блокировок.

Ситуация из практики

В сервисе один путь обновления конфигурации сначала захватывал mutex конфигурации, затем mutex метрик. В обработчике метрик порядок был обратным. Под нагрузкой две горутины зависали, а запуск с -race оставался чистым.

Рассматривались три варианта. Увеличение тайм-аутов только маскировало проблему; добавление случайных задержек помогало иногда воспроизвести её, но делало тест нестабильным; унификация порядка захвата блокировок устраняла сам источник цикла ожидания.

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

Что кандидаты часто упускают

  1. Может ли race detector обнаружить взаимную блокировку, если она возникла из-за mutex?

Нет, сам факт циклического ожидания mutex не является гонкой данных. Race detector может косвенно изменить расписание и тем самым повлиять на вероятность зависания, но это не означает, что он диагностирует deadlock или гарантированно воспроизводит его.

  1. Достаточно ли тайм-аута теста, чтобы найти причину взаимной блокировки?

Нет. Тайм-аут обнаруживает симптом — отсутствие завершения, но не объясняет, какие горутины образовали цикл. Для диагностики нужен стек горутин на момент зависания, анализ удерживаемых блокировок и проверка порядка их захвата.

  1. Почему единый порядок захвата mutex снижает риск deadlock?

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