Возможные причины дедлока (для программ с `std::mutex` и `std::condition_variable`): - Несогласованный порядок захвата нескольких мьютексов (circular wait). Пример: поток A берёт `m1` затем `m2`, поток B — `m2` затем `m1`. - Повторная блокировка одного и того же не‑рекурсивного `std::mutex` в том же потоке (self‑deadlock). - Условие ожидания без проверки предиката или ожидание после уведомления (missed wakeup / lost notification): уведомление произошло до того, как поток вошёл в `wait`, и поток в итоге вечно ждёт. - Ожидание на `condition_variable` с неправильным предикатом (таймер/состояние никогда не устанавливается) — логическая блокировка. - Держать мьютекс во время выполнения действий, которые требуют ответа от другого потока (например, вызов `thread::join`, блокирующий I/O, ожидание другой синхронизации) — взаимная зависимость. - Неправильное использование уведомлений: частые `notify_one` в сложной топологии вместо `notify_all` могут оставить некоторые ожидающие потоки блокированными. - Неправильная комбинация `lock_guard` (который нельзя разблокировать вручную) и `condition_variable::wait`, либо попытка `wait` без `unique_lock`. - Ошибки в обработке исключений — мьютекс не освобождён (редко, если не использовать RAII). Стратегии профилактики: - Всегда проверяйте и документируйте глобальный порядок захвата мьютексов; соблюдайте его во всех потоках. Либо используйте `std::lock(m1, m2, ...)` или `std::scoped_lock` для атомарной блокировки нескольких мьютексов. - Минимизируйте время удержания мьютексов: не выполнять I/O, `join`, долгие вычисления или вызовы пользовательского кода внутри критической секции. - Правильный шаблон ожидания: - Использовать `std::unique_lock lk(m);` - Ждать в цикле или использовать предикат: `cv.wait(lk, []{ return predicate; });` - Никогда не полагаться на единичное `wait` без проверки состояния. - Уведомлять после изменения состояния под мьютексом или (иногда предпочтительнее) освобождая мьютекс перед продолжением работы, но уведомление можно делать и без блокировки; важно изменить состояние гарантированно. - При сомнении использовать `notify_all`, если несколько потоков могут ожидать разного состояния. - Использовать таймауты для критических ожиданий: например `cv.wait_for(lk, std::chrono::milliseconds(100010001000), predicate)` и обрабатывать ложный возврат — это позволяет детектировать «зависшие» ожидания. - Избегать вложенных блокировок; если нужно — применять единый порядок или абстрагировать ресурсы. - При необходимости применять `std::recursive_mutex` осознанно (но лучше переработать логику, чем полагаться на рекурсивный мьютекс). Методы обнаружения и отладки дедлоков: - Инструменты динамического анализа: ThreadSanitizer (TSan), Helgrind/DRD (Valgrind) — показывают гонки и локи; TSan может помочь найти блокировки. - Логирование событий захвата/освобождения мьютексов и уведомлений с таймштампами; затем анализ графа блокировок (lock-order graph) на наличие циклов. - Вставка watchdog‑потока, который по таймауту снимает дампы (stack trace) всех потоков и сигнализирует о застрявших ожиданиях. - Снимки состояния (core dump) и разбор стеков: ищите потоки в состоянии `waiting on condition_variable` или `blocked on mutex` — по стеку видно, кто ждёт и кто удерживает мьютекс. - Юнит/стресс‑тесты с искусственной задержкой и детерминизацией (randomized sleeps, увеличение конкурентности), чтобы воспроизвести редкие сценарии. - Статический анализ и аннотации (clang thread safety analysis) — помогают предотвратить нарушения порядка блокировок. Короткие практические рекомендации: - Используйте `cv.wait(lk, predicate)` (предикат обязателен). - Для нескольких мьютексов — `std::lock` / `std::scoped_lock`. - Не держите мьютексы при вызове `join` или при блокирующем I/O. - Добавляйте таймауты и watchdog для критичных ожиданий. - Регулярно тестируйте с инструментами (TSan, Valgrind) и логами. Если нужно — привожу компактные примеры кода (почему дедлок возникает и как его исправить).
- Несогласованный порядок захвата нескольких мьютексов (circular wait). Пример: поток A берёт `m1` затем `m2`, поток B — `m2` затем `m1`.
- Повторная блокировка одного и того же не‑рекурсивного `std::mutex` в том же потоке (self‑deadlock).
- Условие ожидания без проверки предиката или ожидание после уведомления (missed wakeup / lost notification): уведомление произошло до того, как поток вошёл в `wait`, и поток в итоге вечно ждёт.
- Ожидание на `condition_variable` с неправильным предикатом (таймер/состояние никогда не устанавливается) — логическая блокировка.
- Держать мьютекс во время выполнения действий, которые требуют ответа от другого потока (например, вызов `thread::join`, блокирующий I/O, ожидание другой синхронизации) — взаимная зависимость.
- Неправильное использование уведомлений: частые `notify_one` в сложной топологии вместо `notify_all` могут оставить некоторые ожидающие потоки блокированными.
- Неправильная комбинация `lock_guard` (который нельзя разблокировать вручную) и `condition_variable::wait`, либо попытка `wait` без `unique_lock`.
- Ошибки в обработке исключений — мьютекс не освобождён (редко, если не использовать RAII).
Стратегии профилактики:
- Всегда проверяйте и документируйте глобальный порядок захвата мьютексов; соблюдайте его во всех потоках. Либо используйте `std::lock(m1, m2, ...)` или `std::scoped_lock` для атомарной блокировки нескольких мьютексов.
- Минимизируйте время удержания мьютексов: не выполнять I/O, `join`, долгие вычисления или вызовы пользовательского кода внутри критической секции.
- Правильный шаблон ожидания:
- Использовать `std::unique_lock lk(m);`
- Ждать в цикле или использовать предикат: `cv.wait(lk, []{ return predicate; });`
- Никогда не полагаться на единичное `wait` без проверки состояния.
- Уведомлять после изменения состояния под мьютексом или (иногда предпочтительнее) освобождая мьютекс перед продолжением работы, но уведомление можно делать и без блокировки; важно изменить состояние гарантированно.
- При сомнении использовать `notify_all`, если несколько потоков могут ожидать разного состояния.
- Использовать таймауты для критических ожиданий: например `cv.wait_for(lk, std::chrono::milliseconds(100010001000), predicate)` и обрабатывать ложный возврат — это позволяет детектировать «зависшие» ожидания.
- Избегать вложенных блокировок; если нужно — применять единый порядок или абстрагировать ресурсы.
- При необходимости применять `std::recursive_mutex` осознанно (но лучше переработать логику, чем полагаться на рекурсивный мьютекс).
Методы обнаружения и отладки дедлоков:
- Инструменты динамического анализа: ThreadSanitizer (TSan), Helgrind/DRD (Valgrind) — показывают гонки и локи; TSan может помочь найти блокировки.
- Логирование событий захвата/освобождения мьютексов и уведомлений с таймштампами; затем анализ графа блокировок (lock-order graph) на наличие циклов.
- Вставка watchdog‑потока, который по таймауту снимает дампы (stack trace) всех потоков и сигнализирует о застрявших ожиданиях.
- Снимки состояния (core dump) и разбор стеков: ищите потоки в состоянии `waiting on condition_variable` или `blocked on mutex` — по стеку видно, кто ждёт и кто удерживает мьютекс.
- Юнит/стресс‑тесты с искусственной задержкой и детерминизацией (randomized sleeps, увеличение конкурентности), чтобы воспроизвести редкие сценарии.
- Статический анализ и аннотации (clang thread safety analysis) — помогают предотвратить нарушения порядка блокировок.
Короткие практические рекомендации:
- Используйте `cv.wait(lk, predicate)` (предикат обязателен).
- Для нескольких мьютексов — `std::lock` / `std::scoped_lock`.
- Не держите мьютексы при вызове `join` или при блокирующем I/O.
- Добавляйте таймауты и watchdog для критичных ожиданий.
- Регулярно тестируйте с инструментами (TSan, Valgrind) и логами.
Если нужно — привожу компактные примеры кода (почему дедлок возникает и как его исправить).