Что-то я увлекся рассказами про мьютексы и совсем забыл что не довел рассказ про планирование потоков и приоритеты до конца. Возвращаю должок, рассмотрим интересную задачу из реальной жизни.
🔵 Ситуация
У нас есть 3 потока и каждому назначена своя задача (псевдокод для простоты):
Thread L (low priority):
lock(mutex)
do_some_work()
unlock(mutex)
Thread H (high priority):
lock(mutex)
critical_work()
unlock(mutex)
Thread M (medium priority):
while true:
do_cpu_work()
Что происходит:
- L стартует первым -> захватывает mutex
- H стартует вторым -> пытается захватить mutex -> блокируется, ждёт L
- M стартует третьим -> вытесняет L (приоритет выше) -> крутится на CPU
Итог:
- L не получает CPU, не может освободить mutex.
- H ждёт L, но L вытеснен M.
Следствие: Неэффективная работа программы. Как вы уже поняли из названия эта ситуация и есть инверсия приоритетов - проблема в многозадачных системах, когда задача с высоким приоритетом вынуждена ждать выполнения задачи с низким приоритетом, владеющей необходимым ей ресурсом (мьютексом).
На первый взгляд выглядит странно. В обычных задачах на работе нам не нужно знать про приоритеты потоков. Плюс вытесняющий планировщик выручает переключая контекст и выделяя всем время поработать. Вроде проблема, а вроде и нет🙂
Ответ: Если бы такая ситуация случилась на каком нибудь устройстве от которого ожидают работы в реальном времени то это могла бы быть катастрофа. И такие провалы имеют место в реальности, например на Марсе в 1997м году. Рассказывать долго не хочу, советую статью на хабре с иллюстрациями и контекстом.
🔵 Как разрешить Priority Inversion?
1️⃣ Запрет прерываний в критических секциях
Первое что приходит в голову - запретить прерывать поток находящийся в критической секции (помним что у нас вытесняющее планирование и ОС может прервать поток в любой момент). Но доступна такая магия только в kernel space, поэтому и используется трюк обычно:
- В драйверах для атомарного доступа к hardware registers.
- В критических секциях ядра, чтобы избежать прерывания и race conditions.
Нам нужны механизмы влияния в user-space, поэтому идем дальше.
2️⃣ Протокол пороговых приоритетов (priority ceiling protocol, PCP)
- Каждый mutex имеет "потолочный приоритет" - максимальный приоритет всех потоков, которые могут его захватить.
- Когда поток захватывает такой mutex его приоритет временно повышается до потолка mutex. Это предотвращает вытеснение потока средними по приоритету задачами.
- Когда поток освобождает mutex приоритет возвращается к исходному.
Реализация в POSIX:
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_PROTECT);
3️⃣Протокол наследования приоритетов (priority inheritance protocol, PIP)
Когда H ждёт mutex, который удерживает L:
- L временно получает приоритет H, пока не освободит mutex.
- После unlock приоритет L возвращается к исходному.
Реализация в POSIX:
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
🔵 Что выбрать, PIP или PCP?
- PIP защищает от priority inversion, но допускает deadlock. Довольно просто реализуется, есть везде.
- PCP более строгий, предсказуемый, предотвращает priority inversion и deadlock. Его основной минус - избыточное повышение приоритета. Сложнее в реализации и поддержке.
В реальной жизни используют PIP для стандартных приложений на Linux/RTLinux и PCP для safety-critical embedded или RTOS.
—————
Фух, думаю на этом остановиться. Основные проблемы Concurrency (Deadlock, Race Condition, Readers-Writers, Busy Waiting, Priority Inversion) в рамках цикла постов мы рассмотрели.
Осталось несколько тем и цикл можно завершать. Спасибо что читали, буду рад вашим реакциям и комментариям!
📖 Оглавление
