Что такое инверсия приоритетов (Priority Inversion) в многопоточности?

Ответ

Инверсия приоритетов (Priority Inversion) — это проблемная ситуация в планировании задач с реальным временем (RTOS) или приоритетными очередями, когда задача с высоким приоритетом вынуждена ожидать выполнения задачи с низким приоритетом, что нарушает ожидаемую логику планировщика.

Классический сценарий (на примере трех задач):

  1. Задача L (Low Priority) захватывает разделяемый ресурс (например, блокировку NSLock).
  2. Задача H (High Priority) запускается и также пытается захватить тот же ресурс. Поскольку он занят, H блокируется и ждет.
  3. В этот момент запускается Задача M (Medium Priority), которая не использует общий ресурс. Планировщик, видя, что H заблокирована, а M готова к выполнению, отдает CPU задаче M.
  4. Результат: Задача M (средний приоритет) выполняется, в то время как задача H (высший приоритет) простаивает, ожидая L (низший приоритет). Приоритеты инвертировались.

Пример кода, моделирующего ситуацию:

let sharedResourceLock = NSLock()
let highPriorityQueue = DispatchQueue(label: "high", qos: .userInteractive)
let mediumPriorityQueue = DispatchQueue(label: "medium", qos: .utility)
let lowPriorityQueue = DispatchQueue(label: "low", qos: .background)

// Задача L (низкий приоритет) захватывает ресурс
lowPriorityQueue.async {
    sharedResourceLock.lock()
    print("Low priority task L: Lock acquired")
    sleep(2) // Имитация долгой работы с ресурсом
    sharedResourceLock.unlock()
    print("Low priority task L: Lock released")
}

// Небольшая задержка, чтобы L успел захватить lock
Thread.sleep(forTimeInterval: 0.1)

// Задача H (высокий приоритет) блокируется на ресурсе
highPriorityQueue.async {
    print("High priority task H: Waiting for lock...")
    sharedResourceLock.lock() // БЛОКИРОВКА! Ждет L.
    print("High priority task H: Lock acquired")
    sharedResourceLock.unlock()
}

// Задача M (средний приоритет) может выполняться, пока H ждет
mediumPriorityQueue.async {
    for i in 1...3 {
        print("Medium priority task M: Working... ((i))")
        sleep(1)
    }
}

Решения проблемы:

  • Наследование приоритета (Priority Inheritance): Когда задача H блокируется на ресурсе, удерживаемом L, приоритет L временно повышается до приоритета H. Это позволяет L быстрее завершиться и освободить ресурс. Эта политика используется, например, в pthread_mutex с атрибутом PRIO_INHERIT.
  • Потолок приоритетов (Priority Ceiling): Ресурсу назначается заранее известный высокий "потолочный" приоритет. Любая задача, захватившая этот ресурс, временно получает этот высокий приоритет, предотвращая выполнение задач среднего приоритета.

В iOS/ Swift: Системные механизмы (например, os_unfair_lock) и GCD в определенной степени помогают mitigate эту проблему, но понимание инверсии критично при проектировании собственных сложных систем синхронизации.

Ответ 18+ 🔞

Да ты посмотри, какая дичь, блядь! Сидим мы тут, думаем, что приоритеты — это святое, а они, сука, выкидывают такие фортели, что просто пиздец.

Вот представь, есть у тебя три задачи, как три мужика в бане. Один — крутой мажор, приоритет высоченный (H). Второй — середнячок, не фонтан (M). Третий — последний нищеброд, приоритет ниже плинтуса (L).

И есть у них одна общая шайка-мыло, ресурс, значит. Всё по-братски.

  1. Заходит наш нищеброд L первым, хватает шайку и начинает ею так залихватски мылиться. Не спеша, с чувством.
  2. Тут врывается мажор H. Ему тоже срочно надо помыться. Тыкается к шайке — а она занята! Ну, бля, думает, щас этого лузера попрошу — он же поймёт, кто тут главный. Стоит, ждёт.
  3. И в этот самый момент, пока мажор H упёрся лбом в стенку и ждёт, в парилку вваливается середнячок M. Он вообще без шайки, просто попарить кости пришёл. Планировщик, этот, блядь, банщик, смотрит: мажор-то ждёт, а этот готов! И давай середнячка M в парилку гонять! Тот орет "Ох, жарко!", а дела-то делает.

И что получается, ёпта? Мажор с высшим приоритетом стоит и смотрит, как середнячок среднего приоритета наслаждается жизнью, потому что ждёт он, блядь, от нищеброда низшего приоритета. Приоритеты нахуй перевернулись! Это и есть инверсия, блядь, полный пиздец для систем реального времени. Всё летит к чертям, дедлайны горят, волнение ебать.

Вот код, который эту похабщину демонстрирует:

let sharedResourceLock = NSLock()
let highPriorityQueue = DispatchQueue(label: "high", qos: .userInteractive)
let mediumPriorityQueue = DispatchQueue(label: "medium", qos: .utility)
let lowPriorityQueue = DispatchQueue(label: "low", qos: .background)

// Нищеброд L первый хапнул шайку
lowPriorityQueue.async {
    sharedResourceLock.lock()
    print("Low priority task L: Lock acquired")
    sleep(2) // Зажимает, сука, наслаждается
    sharedResourceLock.unlock()
    print("Low priority task L: Lock released")
}

// Чуть ждем, чтоб L успел схватить
Thread.sleep(forTimeInterval: 0.1)

// Мажор H приперся, а ему не дают
highPriorityQueue.async {
    print("High priority task H: Waiting for lock...")
    sharedResourceLock.lock() // ТУПИК! Ждет этого ушлепка L.
    print("High priority task H: Lock acquired")
    sharedResourceLock.unlock()
}

// А середнячок M в это время вовсю парится
mediumPriorityQueue.async {
    for i in 1...3 {
        print("Medium priority task M: Working... ((i))")
        sleep(1)
    }
}

Как с этой хуйней бороться?

  • Наследование приоритета: Это как взять нашего нищеброда L, который шайку держит, и надеть на него мажорскую цепь. Раз его ждёт мажор H, то и приоритет L временно поднимаем до уровня H. Пусть, сука, мытся быстрее, чтоб не задерживать важную персону! В pthread это, кстати, есть.
  • Потолок приоритетов: Тут ещё проще. Объявляем, что шайка — это святыня. Кто её взял, тот сразу временно становится круче всех. Никакой середнячок M в этот момент даже пикнуть не посмеет.

В iOS, конечно, умные дядьки постарались, чтобы такие косяки реже всплывали, но, блядь, если сам накосячишь с блокировками — получишь эту самую инверсию в чистом виде. Так что думай головой, когда ресурсы делишь, а то будет тебе ебаный цирк, а не многопоточность.