Почему возникает взаимная блокировка (deadlock)?

Ответ

Взаимная блокировка (deadlock) возникает, когда два или более потока или процесса бесконечно ожидают ресурсы, которые заняты друг другом, создавая циклическую зависимость. Это приводит к остановке выполнения всех участвующих компонентов.

Для возникновения deadlock должны одновременно выполняться четыре условия Коффмана:

  1. Взаимное исключение (Mutual Exclusion): Ресурсы не могут быть разделены; только один процесс может использовать ресурс в данный момент.
  2. Удержание и ожидание (Hold and Wait): Процесс, уже владеющий одним или несколькими ресурсами, ожидает получения дополнительных ресурсов, занятых другими процессами.
  3. Отсутствие вытеснения (No Preemption): Ресурсы не могут быть принудительно отобраны у процесса, который ими владеет; они могут быть освобождены только добровольно.
  4. Циклическое ожидание (Circular Wait): Существует замкнутая цепочка процессов, где каждый процесс в цепочке ожидает ресурс, занятый следующим процессом в этой цепочке.

Пример на Python (потоки):

import threading
import time

# Два ресурса (блокировки), которые могут быть захвачены
lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_func1():
    print("Поток 1: Попытка захватить lock1...")
    with lock1:
        print("Поток 1: lock1 захвачен. Попытка захватить lock2...")
        time.sleep(0.1) # Имитация работы
        with lock2:
            print("Поток 1: lock2 захвачен. Выполняю работу.")
    print("Поток 1: Все блокировки освобождены.")

def thread_func2():
    print("Поток 2: Попытка захватить lock2...")
    with lock2:
        print("Поток 2: lock2 захвачен. Попытка захватить lock1...")
        time.sleep(0.1) # Имитация работы
        with lock1:
            print("Поток 2: lock1 захвачен. Выполняю работу.")
    print("Поток 2: Все блокировки освобождены.")

t1 = threading.Thread(target=thread_func1)
t2 = threading.Thread(target=thread_func2)

t1.start()
t2.start()

t1.join()
t2.join()
print("Основной поток завершен.")

В этом примере, если t1 захватит lock1 и t2 захватит lock2 почти одновременно, то t1 будет ждать lock2 (который держит t2), а t2 будет ждать lock1 (который держит t1). Это создает циклическое ожидание, и оба потока заблокируются навсегда.

Предотвращение deadlock часто достигается путем нарушения одного или нескольких из этих четырех условий, например, путем упорядочивания захвата ресурсов или использования таймаутов.

Ответ 18+ 🔞

Так, слушай, про deadlock объяснить? Ну это ж классика, блядь, как в жизни! Представь, сидят два мужика в баре, оба здоровые, оба хотят выпить. Перед каждым стоит полная кружка, но, ёпта, беда — у каждого кружка приклеена к столу с противоположной стороны! Первый тянется к своей, но она у второго перед носом, второй — к своей, но она у первого. И оба сидят, смотрят друг на друга, как дураки, и ждут. Ждут до скончания века, потому что каждый думает: «Сейчас он свою отопрёт, я свою возьму, и всё будет пиздато». А нихуя! Это и есть deadlock, блядь, взаимная блокировка, когда все друг другу встали поперёк горла.

Чтобы эта ебеня случилась, нужно аж четыре условия, как четыре кита, на которых держится этот пиздец. Условия Коффмана, блядь, звучат умно, а суть проще пареной репы:

  1. Взаимное исключение (Mutual Exclusion). Кружка одна, и пить из неё может только один. Нельзя одновременно хлебнуть вдвоём из одной — это ж неблагородно.
  2. Удержание и ожидание (Hold and Wait). Каждый мужик уже держит свою кружку (ну, мысленно), но ждёт, пока освободится чужая, чтобы сделать полный комплект для счастья.
  3. Отсутствие вытеснения (No Preemption). Нельзя просто так взять и вырвать кружку из рук соседа. Это драка будет, а не культурный пивной вечер. Ресурс отдают только добровольно.
  4. Циклическое ожидание (Circular Wait). Вот это самый сок, блядь! Первый ждёт кружку второго, второй ждёт кружку первого. Замкнутый круг, хуле. Пиздец и точка.

А теперь смотри, как это в коде выглядит, этот цирк с конями. Два потока, две блокировки. Один чувак хватает первую, другой — вторую, и оба замирают в позе «ну давай же, сука, отпусти».

import threading
import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_func1():
    print("Поток 1: Попытка захватить lock1...")
    with lock1:
        print("Поток 1: lock1 захвачен. Попытка захватить lock2...")
        time.sleep(0.1) # Притворяется, что работает, а сам просто спит, мудак
        with lock2: # А вот тут он и повиснет, как последний лох!
            print("Поток 1: lock2 захвачен. Выполняю работу.")
    print("Поток 1: Все блокировки освобождены.")

def thread_func2():
    print("Поток 2: Попытка захватить lock2...")
    with lock2:
        print("Поток 2: lock2 захвачен. Попытка захватить lock1...")
        time.sleep(0.1) # Тоже валяет дурака
        with lock1: # И этот туда же! Висеть будем вместе, блядь!
            print("Поток 2: lock1 захвачен. Выполняю работу.")
    print("Поток 2: Все блокировки освобождены.")

t1 = threading.Thread(target=thread_func1)
t2 = threading.Thread(target=thread_func2)

t1.start()
t2.start()

t1.join()
t2.join()
print("Основной поток завершен.")

Запустишь этот код — и он просто повиснет, ебать его в сраку. В консоли выведет первые две строчки от каждого потока и... тишина. Вечность. Потому что t1 схватил lock1 и ждёт lock2, а t2 в это время уже успел прихватить lock2 и ждёт lock1. И оба сидят, улыбаются, как мартышлюшки.

Как с этим бороться? Да нарушить любое из этих четырёх условий, и всё рассосётся! Самый простой способ — брать ресурсы в одном и том же порядке всем подряд. Если бы оба потока сначала всегда хватали lock1, а потом уже lock2, то второму просто пришлось бы подождать, пока первый всё сделает и отпустит оба. Никакого круга, всё по очереди, как в нормальной очереди за водкой. Или ставить таймауты — не получилось захватить за N секунд, отпусти всё, что держишь, отойди, подумай о жизни и попробуй снова. Главное — не допускать этой ебаной цикличности, когда все друг на друга упёрлись и ждут чуда. Чуда не будет, будет только deadlock, ёпта.