Поведение threading.Lock при повторном acquire(timeout=0)

Поведение threading.Lock при повторном acquire(timeout=0)

@python_quiz

Разберем этот квиз

Коротко: в стандартном (некурсивном) threading.Lock повторный неблокирующий захват тем же потоком не проходит — он вернёт False. Ниже объясняю почему, разбираю варианты и привожу демонстрации.

Условие (фрагмент кода)

python

Этот код: поток захватывает lock один раз, затем пытается снова захватить тот же lock, но с timeout=0 — т.е. неблокирующая попытка. Что будет напечатано?

Ключевые моменты поведения threading.Lock

  • threading.Lock — это примитивный (не рекурсивный) мьютекс. Он просто в состояниях locked/unlocked и не позволяет одному и тому же потоку рекурсивно захватить его несколько раз.
  • acquire(timeout=0) в Python является эквивалентом неблокирующего захвата (аналогично acquire(blocking=False)). Метод возвращает True при успешном захвате, False — если захват не выполнен.
  • Если вызвать acquire() (по умолчанию blocking=True) второй раз в том же потоке и не использовать timeout, поток заблокируется до тех пор, пока lock не будет освобождён (в данном примере это приведёт к взаимной блокировке, потому что тот же поток уже держит lock и не собирается его освобождать перед вторым acquire).

Разбор вариантов (почему так)

  • "True — второй захват пройдёт успешно"

Неверно. Для обычного Lock повторный захват тем же потоком не допускается, поэтому неблокирующая попытка не увенчается успехом.

  • "False — не удалось захватить, вернётся False"

Верно для сценария с timeout=0: возвращается False, потому что lock уже занят.

  • "Вызовет RuntimeError при повторном acquire"

Неверно. RuntimeError возникает при некорректной работе с release (например, при попытке освободить незахваченный lock), но повторный acquire на уже захваченном lock не вызывает исключение — он либо блокирует, либо (в неблокирующем режиме) возвращает False.

  • "Поток навсегда заблокируется на втором acquire"

Возможно в случае blocking=True (по умолчанию) — если вызвать acquire() во второй раз и никогда не освободить lock, поток действительно будет ждать бесконечно. Но в нашем конкретном коде используется timeout=0 (неблокирующий), так что бесконечной блокировки не будет.

Демонстрация поведения

Исходный пример (ожидаемый вывод):

python

Ожидаемый вывод:

Эквивалент с явным blocking=False:

python

Если нужен рекурсивный (reentrant) мьютекс, используйте RLock:

python

Ожидаемый вывод:

Пояснение: RLock отслеживает владельца и счётчик захватов, поэтому один и тот же поток может захватить его несколько раз и должен освободить столько же раз.

Практические рекомендации

  • Если нужно разрешать повторный захват одним и тем же потоком (например, когда один метод вызывает другой, который тоже берёт тот же мьютекс), выбирайте threading.RLock.
  • Для попыток "сразу взять, если свободен" используйте acquire(blocking=False) или acquire(timeout=0).
  • Будьте осторожны с блокирующим acquire() без таймаута — легко получить взаимную блокировку (deadlock), если не продумано освобождение.
  • При отладке конкуренции полезно явно логировать моменты acquire/release и использовать таймауты, чтобы не зависать в тестах.

Вывод

В данном фрагменте второй неблокирующий acquire вернёт False, потому что threading.Lock не рекурсивен и уже захвачен тем же потоком. Если требуется рекурсивное поведение — используйте threading.RLock.

Report Page