Перейти к содержанию

Взаимная блокировка (deadlock)

Задача A взяла мьютекс X и ждёт Y. Задача B взяла Y и ждёт X. Ни одна не отпустит своё, пока не получит чужое, и обе будут ждать вечно. Это взаимная блокировка — одна из классических ошибок многозадачных программ.

Обновлено Редакция GAW

Взаимная блокировка (deadlock, тупик) — состояние, в котором две или больше задачи не могут продолжать работу, потому что каждая ждёт ресурс, захваченный другой. Ни одна не отпустит свой ресурс, пока не получит чужой, и без внешнего вмешательства ожидание длится вечно.

Сценарий из руководства FreeRTOS

Задачам A и B для работы нужны два мьютекса, X и Y:

  1. Задача A берёт мьютекс X.
  2. Задачу A вытесняет задача B.
  3. B берёт Y и пытается взять X — он занят, B блокируется.
  4. A продолжает работу и пытается взять Y — он занят, A блокируется.

Теперь A ждёт B, а B ждёт A.

Как избежать

ПриёмКак работает
Единый порядок захватавсе задачи берут X раньше Y — цикл ожидания не может возникнуть
Таймаут вместо вечного ожиданиянеудача по таймауту видна и обрабатывается
Задача-привратникодин владелец ресурса, остальные шлют ему запросы через очередь
Рекурсивный мьютексзадача может повторно взять уже свой мьютекс

Руководство FreeRTOS считает лучшим способом продумать возможность тупика ещё при проектировании и отмечает, что в небольших встроенных системах это обычно удаётся: разработчик видит всё приложение целиком. Ждать мьютекс без таймаута — по тому же руководству, обычно плохая практика.

Блокировка самого себя

Задача может заблокировать и сама себя, если повторно возьмёт уже захваченный ею обычный мьютекс. Для вложенных вызовов во FreeRTOS есть рекурсивный мьютекс: xSemaphoreCreateRecursiveMutex(), xSemaphoreTakeRecursive() и xSemaphoreGiveRecursive().

Последний рубеж

Если тупик всё же случился в поле, систему выводит из него сторожевой таймер: задачи перестают отчитываться, и таймер перезапускает микроконтроллер. Как построить контроль нескольких задач одним таймером — в статье «Сторожевой таймер: IWDG, WWDG и WDT AVR». Родственная проблема при работе с мьютексами — инверсия приоритетов.

Частые вопросы

Как обнаружить взаимную блокировку в работающем устройстве?

Ждать мьютекс не бесконечно, а с таймаутом, немного большим ожидаемого времени удержания. Руководство FreeRTOS так и советует: тогда неудача взять мьютекс — сигнал об ошибке проектирования, её можно записать в журнал. Последний рубеж — сторожевой таймер, который перезапустит систему, если задачи перестали отчитываться.

Может ли задача заблокировать саму себя?

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

Бывают ли взаимные блокировки без RTOS?

Бывают в другой форме: основной цикл ждёт флаг, который должно выставить прерывание, а прерывание запрещено, потому что цикл вошёл в критическую секцию. Принцип тот же — каждый ждёт то, что может дать только другой.

Источники

  1. 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: задача-привратник даёт взаимное исключение без риска тупика
  2. FreeRTOS Kernel V11.1.0, include/semphr.h (GitHub FreeRTOS/FreeRTOS-Kernel) — xSemaphoreTake() принимает время ожидания в тиках; мьютекс, созданный xSemaphoreCreateMutex(), нельзя брать рекурсивными макросами; xSemaphoreCreateRecursiveMutex() — для рекурсивного захвата

Нашли ошибку?