Поведение threading.Lock при повторном acquire(timeout=0)
@python_quizРазберем этот квиз
Коротко: в стандартном (некурсивном) threading.Lock повторный неблокирующий захват тем же потоком не проходит — он вернёт False. Ниже объясняю почему, разбираю варианты и привожу демонстрации.
Условие (фрагмент кода)

Этот код: поток захватывает 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 (неблокирующий), так что бесконечной блокировки не будет.
Демонстрация поведения
Исходный пример (ожидаемый вывод):

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

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

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

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

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