Критическая секция и взаимное исключение (mutex/lock)
Лекарство от гонок: пускать в опасный участок кода только один поток за раз.
Критическая секция — участок кода, обращающийся к общему ресурсу, который в любой момент должен выполнять не более одного потока; взаимное исключение (mutual exclusion) — гарантия этого свойства.
Раз гонка возникает при одновременном доступе к общим данным, решение очевидно: запретить одновременность именно в этом месте. Инструмент для этого — мьютекс (mutex, от mutual exclusion), он же блокировка (Lock).
Как работает мьютекс
Мьютекс похож на ключ от переговорной комнаты. Поток, который хочет войти в критическую секцию, должен сначала захватить блокировку (acquire). Пока ключ у него, остальные ждут. Закончив, поток освобождает блокировку (release), и ключ забирает следующий.
lock = Lock()
lock.acquire() # взял ключ
counter += 1 # критическая секция: я тут один
lock.release() # отдал ключВ Python безопаснее использовать контекстный менеджер with — он сам вызовет release, даже если внутри возникнет исключение:
import threading
lock = threading.Lock()
counter = 0
def increment():
global counter
with lock: # acquire на входе, release на выходе
counter += 1
threads = [threading.Thread(target=increment) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # теперь стабильно 1000Этот код использует реальные потоки, поэтому в браузере не запускается. Зато логику «один ключ — одна комната» легко смоделировать:
lock_taken = False
counter = 0
def enter_critical(name):
global lock_taken, counter
if lock_taken:
print(f"{name}: жду, ключ занят")
return
lock_taken = True
counter += 1 # критическая секция
print(f"{name}: вошёл, counter = {counter}")
lock_taken = False
enter_critical("A")
enter_critical("B")Вывод:
A: вошёл, counter = 1 B: вошёл, counter = 2
Свойства правильного взаимного исключения
- Безопасность: в критической секции максимум один поток.
- Прогресс: если секция свободна, кто-то из желающих обязательно войдёт.
- Без голодания: ни один поток не ждёт ключ вечно.
Как работает под капотом
Внутри мьютекс опирается на атомарные аппаратные инструкции вроде compare-and-swap, которые процессор гарантированно выполняет целиком, без вклинивания. Если ключ занят, поток обычно переходит в состояние «заблокирован», и планировщик не тратит на него кванты, пока ключ не освободится. Это эффективнее, чем «крутиться» в цикле и постоянно проверять.
Частые ошибки
- Забыть release. Если поток взял ключ и не отдал (например, из-за исключения), остальные зависнут навсегда. Используйте
with. - Слишком широкая критическая секция. Если держать блокировку дольше нужного, потоки выстраиваются в очередь и параллелизм исчезает.
- Разные блокировки на один ресурс. Если два потока защищают одни данные разными мьютексами, защиты нет.
Итог
- Критическая секция — код, который должен выполнять один поток за раз.
- Мьютекс даёт взаимное исключение через acquire/release.
- В Python используйте
with lock:— он надёжно освобождает блокировку. - Держите критическую секцию как можно короче.