Взаимная блокировка (deadlock)
Задача A взяла мьютекс X и ждёт Y. Задача B взяла Y и ждёт X. Ни одна не отпустит своё, пока не получит чужое, и обе будут ждать вечно. Это взаимная блокировка — одна из классических ошибок многозадачных программ.
Обновлено Редакция GAW
Взаимная блокировка (deadlock, тупик) — состояние, в котором две или больше задачи не могут продолжать работу, потому что каждая ждёт ресурс, захваченный другой. Ни одна не отпустит свой ресурс, пока не получит чужой, и без внешнего вмешательства ожидание длится вечно.
Сценарий из руководства FreeRTOS
Задачам A и B для работы нужны два мьютекса, X и Y:
- Задача A берёт мьютекс X.
- Задачу A вытесняет задача B.
- B берёт Y и пытается взять X — он занят, B блокируется.
- A продолжает работу и пытается взять Y — он занят, A блокируется.
Теперь A ждёт B, а B ждёт A.
Как избежать
| Приём | Как работает |
|---|---|
| Единый порядок захвата | все задачи берут X раньше Y — цикл ожидания не может возникнуть |
| Таймаут вместо вечного ожидания | неудача по таймауту видна и обрабатывается |
| Задача-привратник | один владелец ресурса, остальные шлют ему запросы через очередь |
| Рекурсивный мьютекс | задача может повторно взять уже свой мьютекс |
Руководство FreeRTOS считает лучшим способом продумать возможность тупика ещё при проектировании и отмечает, что в небольших встроенных системах это обычно удаётся: разработчик видит всё приложение целиком. Ждать мьютекс без таймаута — по тому же руководству, обычно плохая практика.
Блокировка самого себя
Задача может заблокировать и сама себя, если повторно возьмёт уже захваченный ею обычный мьютекс. Для вложенных вызовов во FreeRTOS есть рекурсивный мьютекс: xSemaphoreCreateRecursiveMutex(), xSemaphoreTakeRecursive() и xSemaphoreGiveRecursive().
Последний рубеж
Если тупик всё же случился в поле, систему выводит из него сторожевой таймер: задачи перестают отчитываться, и таймер перезапускает микроконтроллер. Как построить контроль нескольких задач одним таймером — в статье «Сторожевой таймер: IWDG, WWDG и WDT AVR». Родственная проблема при работе с мьютексами — инверсия приоритетов.
Частые вопросы
Как обнаружить взаимную блокировку в работающем устройстве?
Ждать мьютекс не бесконечно, а с таймаутом, немного большим ожидаемого времени удержания. Руководство FreeRTOS так и советует: тогда неудача взять мьютекс — сигнал об ошибке проектирования, её можно записать в журнал. Последний рубеж — сторожевой таймер, который перезапустит систему, если задачи перестали отчитываться.
Может ли задача заблокировать саму себя?
Да: если она взяла обычный мьютекс и, не отдав его, пытается взять снова — например, функция с мьютексом вызвала сама себя или другую функцию с тем же мьютексом. Для таких случаев во FreeRTOS есть рекурсивный мьютекс: его можно брать повторно, а освободится он после стольких же возвратов.
Бывают ли взаимные блокировки без RTOS?
Бывают в другой форме: основной цикл ждёт флаг, который должно выставить прерывание, а прерывание запрещено, потому что цикл вошёл в критическую секцию. Принцип тот же — каждый ждёт то, что может дать только другой.
Источники
- R. Barry, FreeRTOS Team. Mastering the FreeRTOS Real Time Kernel — A Hands-On Tutorial Guide, Release 1.1.0 (Amazon Web Services, 2023) — разд. 8.3.4 Deadlock (or Deadly Embrace): две задачи не могут продолжать, потому что каждая ждёт ресурс, который держит другая; сценарий: A берёт X, вытесняется B, B берёт Y и ждёт X, A ждёт Y; лучший способ — исключить на этапе проектирования; ждать мьютекс бесконечно — обычно плохая практика, таймаут немного больше ожидаемого — тогда неудача сигнализирует об ошибке проектирования; 8.3.5: задача может заблокировать сама себя, повторно взяв обычный мьютекс, — для этого рекурсивные мьютексы (xSemaphoreCreateRecursiveMutex, Take/GiveRecursive); 8.4: задача-привратник даёт взаимное исключение без риска тупика
- FreeRTOS Kernel V11.1.0, include/semphr.h (GitHub FreeRTOS/FreeRTOS-Kernel) — xSemaphoreTake() принимает время ожидания в тиках; мьютекс, созданный xSemaphoreCreateMutex(), нельзя брать рекурсивными макросами; xSemaphoreCreateRecursiveMutex() — для рекурсивного захвата
Нашли ошибку?
Смотрите также
- Мьютекс (mutex)Объект RTOS для взаимного исключения задач при доступе к общему ресурсу
- Инверсия приоритетовЗадача с высоким приоритетом ждёт задачу с низким из-за общего ресурса
- Сторожевой таймерWatchdog: сброс МК при зависании программы
- Семафор (двоичный и счётный)Объект RTOS для сигнала о событиях и учёта свободных ресурсов
- Сторожевой таймерWDT AVR, IWDG и WWDG STM32, таймаут, окно, контроль задач, причина сброса