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