Поведение non-blocking acquire в Python-потоках
@python_quizРазберем этот квиз
Коротко: код вызывает нек-блокирующее lock.acquire(blocking=False) по одному разу в каждом потоке. Печатаемое значение счётчика может быть 1, 2 или 3 — всё зависит от того, в какой момент разные потоки попытаются захватить lock. Ниже — подробный разбор.
Исходный код

Что здесь происходит (пошагово)
- lock — обычный threading.Lock(), изначально свободен.
- c — список с единственным элементом, используется как контейнер для изменяемого целого (int неизменяем, поэтому list удобен).
- Каждому потоку в target передана лямбда-выражение, которое делает:
- вызывает lock.acquire(blocking=False).
- Если acquire вернул True (захват удался), выполняется вторая часть выражения:
- c.__setitem__(0, c[0] + 1) — увеличиваем счётчик.
- Оператор or приводит к выполнению lock.release() (потому что setitem возвращает None, а None — False).
- Каждый поток вызывает acquire только один раз; если в момент вызова lock занят — acquire вернёт False и поток ничего не сделает.
Важно: c.__setitem__ выполняется до lock.release(), то есть инкремент происходит под защитой локa — корректно для одного успешного захвата.
Почему результат может быть 1, 2 или 3
- В начале lock свободен, поэтому первый поток, который вызовет acquire, гарантированно захватит lock и увеличит c[0].
- Остальные потоки тоже попытаются выполнить acquire, но их попытки могут быть выполнены:
- До того, как первый поток успеет захватить lock — тогда они тоже успеют захватить (в порядке планировщика), или
- Во время того, как lock уже занят — тогда их попытка вернёт False и они не будут повторять попытку, т.е. не увеличат счётчик.
- Поскольку каждый поток делает одну попытку, итог зависит от того, в какие моменты планировщик даст им CPU. Поэтому возможные результаты: 1, 2 или 3.
- Результат 0 невозможен: хотя некоторые потоки могут провалить acquire, по крайней мере один поток выполнит попытку первым и захватит свободный lock.
Разбор предложенных вариантов (логическое объяснение)
- Утверждение "0 — невозможно, по крайней мере один поток получит lock" — верно в том смысле, что 0 действительно невозможен.
- Утверждение "Точно 3, все потоки гарантированно увеличат счётчик" — неверно: без блокирующей повторной попытки потоки, начавшие свою попытку в момент, когда lock занят, не повторяют её и не увеличивают счётчик.
- Утверждение "1, 2 или 3, в зависимости от планировщика потоков" — соответствует реальному поведению программы и объясняется выше.
- Утверждение "Точно 1, только один поток сможет захватить lock" — неверно: возможно, что разные потоки вызовут acquire в разные моменты и захватят lock (если их попытки не пересекаются).
Улучшение читаемости и альтернативные варианты
Текущий стиль (компактная лямбда с логическими операторами) труден для понимания. Вот более явный и понятный вариант:

Если нужно, чтобы все три потока гарантированно увеличили счётчик (результат всегда 3), используйте блокирующий acquire (без blocking=False):

Если хотите, чтобы каждый поток пытался до успеха (циклически), используйте цикл с короткой паузой или блокирующий acquire.
Заключение
Ключевой момент: acquire(blocking=False) делает только одну, негарантированную попытку захватить lock. Поскольку каждый поток вызывает эту попытку ровно один раз, итоговая сумма успешных захватов зависит от порядка и времени вызовов — возможны 1, 2 или 3. Чтобы получить детерминированный результат (все успешные захваты или гарантированное их количество), нужно менять стратегию (например, использовать блокирующий acquire или повторять попытки).