Skip to content

Deadlock - Мертвая блокировка

Блокировка методами Lock()

  • Взаимная блокировка (Deadlock): Если два или более потока захватывают несколько блокировок в разном порядке, это может привести к взаимной блокировке, когда каждый поток ждет освобождения ресурса, захваченного другим потоком.
  • Забытие освобождения блокировки: Если блокировка захватывается, но не освобождается из-за ошибки в коде или исключения, это может привести к зависанию программы. Использование контекстного менеджера (with lock) помогает избежать этой проблемы.

Блокировка при использовании with

Мертвая блокировка может возникнуть и при использовании контекстных менеджеров with, если порядок захвата блокировок не соблюдается.

python
from threading import Thread, Lock
import time

def go_thread_1():
    with lock_1:
        print("\nThread 1 acquired lock_1", time.time(), end='')
        time.sleep(1)
        with lock_2:
            print("\nThread 1 acquired lock_2", time.time(), end='')
            time.sleep(1)

def go_thread_2():
    with lock_2:
        print(f"\nThread 2 acquired lock_2", time.time(), end='')
        time.sleep(1)
        with lock_1:
            print("\nThread 2 acquired lock_1", time.time(), end='')
            time.sleep(1)

lock_1 = Lock()
lock_2 = Lock()
Thread(target=go_thread_1).start()
Thread(target=go_thread_2).start()

Вывод:

Thread 1 acquired lock_1 1731336810.119597
Thread 1 acquired lock_2 1731336812.1361372

После этого программа зависает, поскольку каждый поток ожидает освобождения второго замка, удерживаемого другим потоком. В этом примере поток 1 захватывает lock_1 и пытается захватить lock_2, а поток 2 захватывает lock_2 и пытается захватить lock_1. Это приводит к мертвой блокировке, так как каждый поток ожидает освобождения блокировки, которую держит другой поток.

Чтобы избежать мертвой блокировки, можно использовать одинаковый порядок захвата блокировок во всех потоках:

python
from threading import Thread, Lock
import time

def go_thread_1():
    with lock_1:
        print("\nThread 1 acquired lock_1", time.time(), end='')
        time.sleep(2)
        with lock_2:
            print("\nThread 1 acquired lock_2", time.time(), end='')
            time.sleep(1)

def go_thread_2():
    with lock_1:
        print(f"\nThread 2 acquired lock_1", time.time(), end='')
        time.sleep(1)
        with lock_2:
            print("\nThread 2 acquired lock_2", time.time(), end='')
            time.sleep(1)

lock_1 = Lock()
lock_2 = Lock()
Thread(target=go_thread_1).start()
Thread(target=go_thread_2).start()

Вывод:

Thread 1 acquired lock_1 1731336810.119597
Thread 1 acquired lock_2 1731336812.1361372
Thread 2 acquired lock_1 1731336813.1445749
Thread 2 acquired lock_2 1731336814.1527731

Теперь оба потока захватывают блокировки в одном и том же порядке, что предотвращает мертвую блокировку.

Обработка ошибок блокировки

При работе с блокировками могут возникать различные ошибки, такие как попытка захватить уже захваченную блокировку, освободить не захваченную блокировку или превышение времени ожидания. Эти ошибки могут привести к непредсказуемому поведению программы.

Использование конструкции try, except и finally позволяет гарантировать, что блокировка будет освобождена, даже если в процессе выполнения возникнет исключение.

python
import threading
import time

lock = threading.Lock()

def critical_section():
    print("\nCritical section entered")
    try:
        lock.acquire()
        print("Critical section continue")
        raise ValueError("Error in a thread!")
    except Exception as e:
        print(f"Exception in the thread: {e}")
    finally:
        print(lock.locked())
        lock.release()
        print(lock.locked())
        print("The thread release.")

# Создаем два потока
threading.Thread(target=critical_section).start()
time.sleep(1)
threading.Thread(target=critical_section).start()

Вывод:

Critical section entered
Critical section continue
Exception in the thread: Error in a thread!
The thread release.

Critical section entered
Critical section continue
Exception in the thread: Error in a thread!
The thread release.

В этом примере блокировка захватывается в блоке try, и даже если в критической секции возникнет исключение, блокировка будет освобождена в блоке finally. Это гарантирует, что блокировка не останется захваченной, иначе это привело бы к мертвой блокировке или другим проблемам.

lock.release() - если строку закомментировать или она будет отсутствовать, то это приведет к зависанию потока и программы.

Использование блоков try, except и finally с контекстным менеджером with:

python
from threading import Thread, Lock

def worker():
    try:
        with lock:
            print("Thread: Acquired lock")
            raise Exception("Something went wrong")
    except Exception as e:
        print(f"Thread: Caught an exception: {e}")
    finally:
        print("Thread: Releasing lock")
        print(lock.locked())
        # lock.release()    # RuntimeError: release unlocked lock

lock = Lock()
Thread(target=worker).start()

Пояснение работы кода:

  • try: Блок, где может возникнуть ошибка.
  • except: Обрабатывает возникшую ошибку, предотвращая крах потока.
  • finally: Блок, который выполнится в любом случае, гарантируя освобождение ресурсов (в данном случае, замка).

Решение проблемы взаимной блокировки

Для предотвращения deadlock можно использовать различные стратегии:

  • Порядок захвата блокировок: Все потоки должны захватывать блокировки в одном и том же порядке.
  • Timeout (таймаут): Вместо блокировки на неопределенный срок можно использовать acquire(timeout) для попытки захвата блокировки с таймаутом.

Рассмотрим пример использования методов .acquire(blocking, timeout) и .release(), который предоставляет класс Lock для синхронизации доступа к общим ресурсам в многопоточной среде.

python
from threading import Thread, Lock
import time

lock = Lock()
amount_increased = 0
amount_rejected = 0

def worker(name):
    global amount_increased, amount_rejected
    # Попытка захвата блокировки с таймаутом в 2 секунды
    if not lock.acquire(blocking=True, timeout=2):
        print(f"\n{name} failed to acquire lock for 2 seconds.", end='')
        amount_rejected += 1
        return
    try:
        print(f"\n{name} acquire lock.", end='')
        local_counter = amount_increased
        time.sleep(1)
        amount_increased = local_counter + 1
        print(f"\n{name} increase count until {amount_increased}.", end='')
    finally:
        lock.release()  # Освобождение блокировки
        print(f"\n{name} release lock.", end='')


threads = []
for index in range(5):
    thread = Thread(target=worker, args=(f"Thread {index+1}",))
    thread.start()
    threads.append(thread)

[thread.join() for thread in threads]
print(f"\nFinally counter values:\n{amount_increased = }\n{amount_rejected = }")

Метод .acquire(blocking, timeout):

  • blocking=True означает, что поток будет ждать блокировку, пока она не будет освобождена другим потоком, если она уже захвачена.
  • timeout=2 указывает, что поток будет ждать не более 2 секунд. Если за это время блокировка не будет получена, метод вернет False.

Метод .release():

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

Работа потоков:

  • Каждый поток пытается захватить блокировку. Если блокировка уже занята, поток ждет её освобождения, если прошло 2 секунды, поток выполняет операции с общим ресурсом amount_rejected, пропуская критическую секцию.
  • Если блокировка захвачена, поток выполняет операции с общим ресурсом amount_increased, после чего освобождает блокировку.

Этот пример демонстрирует, как можно контролировать доступ к общим ресурсам в многопоточной среде, предотвращая состояния гонки race conditions и обеспечивая корректную работу с общими данными.

Потенциальные проблемы с блокировками

  • Взаимная блокировка (Deadlock): Если два или более потока захватывают несколько блокировок в разном порядке, это может привести к взаимной блокировке, когда каждый поток ждет освобождения ресурса, захваченного другим потоком.
  • Забытие освобождения блокировки: Если блокировка захватывается, но не освобождается из-за ошибки в коде или исключения, это может привести к зависанию программы. Использование контекстного менеджера with lock помогает избежать этой проблемы.

Заключение

Многопоточное программирование предоставляет инструменты для повышения производительности, но требует внимательного подхода к синхронизации и обработке ошибок. Использование блокировок Lock позволяет предотвратить состояния гонки и мертвые блокировки, а конструкции try и finally обеспечивают надежное управление ресурсами.

Упражнения

  1. Напишите программу, которая создает три потока. Каждый поток должен увеличивать глобальную переменную counter на 100000 раз. Используйте блокировку для предотвращения состояния гонки.
  2. Напишите программу, которая создает два потока. Каждый поток должен читать данные из файла и записывать их в другой файл. Используйте блокировку, чтобы предотвратить конфликты при записи в файл.
  3. Напишите программу, которая создает пять потоков. Каждый поток должен выполнять задачу, которая занимает случайное количество времени (от 1 до 5 секунд). Используйте блокировку и конструкцию try и finally, чтобы гарантировать, что все потоки завершатся корректно.
  4. Мертвая блокировка с тремя потоками: - Создайте три потока, каждый из которых захватывает три замка в разном порядке. Продемонстрируйте мертвую блокировку и предложите способ ее избежания.
  5. Использование таймаута для предотвращения мертвой блокировки: - Модифицируйте пример мертвой блокировки, используя acquire с таймаутом. Покажите, как это может помочь избежать мертвой блокировки.
  6. Обработка ошибок с несколькими блокировками: - Напишите программу, где поток захватывает несколько блокировок и обрабатывает ошибки в каждой из них, используя try-except-finally.

Contacts: teffal@mail.ru