Поведение non-blocking acquire в Python-потоках

Поведение non-blocking acquire в Python-потоках

@python_quiz

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

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

Исходный код

python

Что здесь происходит (пошагово)

  1. lock — обычный threading.Lock(), изначально свободен.
  2. c — список с единственным элементом, используется как контейнер для изменяемого целого (int неизменяем, поэтому list удобен).
  3. Каждому потоку в target передана лямбда-выражение, которое делает:

- вызывает lock.acquire(blocking=False).
- Если acquire вернул True (захват удался), выполняется вторая часть выражения:
- c.__setitem__(0, c[0] + 1) — увеличиваем счётчик.
- Оператор or приводит к выполнению lock.release() (потому что setitem возвращает None, а None — False).

  1. Каждый поток вызывает 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 (если их попытки не пересекаются).

Улучшение читаемости и альтернативные варианты

Текущий стиль (компактная лямбда с логическими операторами) труден для понимания. Вот более явный и понятный вариант:

python

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

python

Если хотите, чтобы каждый поток пытался до успеха (циклически), используйте цикл с короткой паузой или блокирующий acquire.

Заключение

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

Report Page